Дисклеймер

Данная статья будет полезна новичкам и, возможно, старичкам. Эта реализация является чисто субъективной и может вам не понравиться (жду вас в комментах). Для понимания материала требуются базовые навыки работы с React и TypeScript.

Введение

Styled Components — одно из популярных решений написания кода методом CSS in JS. Гибкое, простое и, главное, идеально вписывается в архитектуру React приложения.

CSS in JS — описание стилей в JavaScript файлах.

Преимущества:

  1. Никаких больше className. Возможность передавать классы никуда не пропадает, но их использование опционально и бессмысленно, теперь мы можем прописывать все стили внутри стилизованных компонент, и классы будут генерироваться автоматически.

  2. Простая динамическая стилизация. Не нужно больше писать тернарные операторы и жонглировать className внутри компоненты, теперь все эти проблемы решаются благодаря прокидыванию пропсов внутрь стилизованных компонент.

  3. Теперь это JS. Так как теперь стили пишутся в экосистеме JavaScript, это упрощает навигацию по проекту и даёт различные возможности написания кода.

  4. StylisJS под капотом. Данный препроцессор поддерживает:
    4.1. Ссылки на родителя &, который часто используют в SCSS.
    4.2. Минификация — уменьшение размера исходного кода.
    4.3. Tree Shaking — удаление мёртвого кода.
    4.4. Вендорные префиксы  приставка к свойству CSS, обеспечивающая поддержку браузерами, в которых определённая функция ещё не внедрена на постоянной основе.

За последние пол года Styled Components стал моим любимчиком, и теперь я стараюсь использовать его в каждом возможном проекте. В этой статье хочу выделить мои лучшие практики и поделиться приобретенным опытом.

Установка

Создадим приложение с помощью CRA с TypeScript.

npx create-react-app my-app --template typescript
# or
yarn create react-app my-app --template typescript

TypeScript будет отличным помощником в написании стилизованных компонент и даст нам больше контроля и понимания в коде. Все дальнейшие примеры будут описываться в связке с TypeScript.

Установим Styled Components и типы для него.

npm i styled-components @types/styled-components
# or
yarn add styled-components @types/styled-components

Установим расширение vscode-styled-components для подсветки и подсказок в VSCode.

https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components

Создадим следующую файловую структуру в корне src. Чуть позже мы остановимся на каждом из этих файлов.

--src/
---styles/
----animations.ts
----components.ts
----global.ts
----theme.ts

Основы

Базовый пример

Посмотрим на простую реализацию Styled Components.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

// Создаем стилизованную компоненту
// Присваиваем ей функцию styled.[название тега]
// Приписываем шаблонную строку и внутри пишем CSS стили
const Container = styled.div`
  background-color: #2b2b2b;
  border-radius: 5px;
`

const Title = styled.h1`
  font-weight: 300;
`
const Text = styled.p`
  font-size: 12px
`

// Используем эти компоненты внутри нашего JSX!
export const SimpleComponent = () => (
  <Container>
    <Title>Styled Component</Title>
    <Text>Some text</Text>
  </Container>
)

Зависимости (properties/пропсы)

Чтобы сделать нашу стилизованную компоненту зависимой от значений, нужно передать атрибутом необходимые параметры.

В случае с TypeScript для описания дополнительных свойств нужно определить тип.

styled.[название тега]<тип>`стили`

Обобщённый тип (обобщение, дженерик) позволяет резервировать место для типа, который будет заменён на конкретный, переданный пользователем в треугольных скобках <тип>.

// Какая-тоКомпонента.tsx

import styled from "styled-components";

// Компонента <Container/> будет ждать на вход
// атрибут bg с любым строковым значением
const Container = styled.div<{bg: string}>`

  // Чтобы получить доступ к зависимостям, 
  // внутри шаблонных строк воспользуемся строковой интерполяцией `${...}`
  // Где вызовем функцию у которой есть параметр props
  background-color: ${props => props.bg};
`

// Если тип занимает много места, 
// то будет лучше вынести его в отельный интерфейс
interface TitleProps {
  weight: 200 | 300 | 400 | 500 | 600 | 700
}
const Title = styled.h1<TitleProps>`
  // Для лучшей читаемости - деструктурируем props,
  // задаем дефолтное значение если это необходимо
  font-weight: ${({ weight = 400 }) => weight};
`

interface TextProps {
  primary: boolean
}
const Text = styled.p<TextProps>`
  color: ${({ primary }) => primary ? '#424242' : '4b4b4b'};
`

export const SimpleComponentWithProps = () => (
  <Container bg='#fcfcfc'>
    <Title weight={300}>Styled Component</Title>
    <Text primary>Some Text</Text>
  </Container>
)

Атрибуты

Мы также имеем доступ к атрибутам, когда создаём стилизованную компоненту. Метод attrs позволяет преобразовывать пропсы стилизованной компоненты перед их использованием.

// Какая-тоКомпонента.tsx

import styled from "styled-components";

interface TextInputProps {
  size: number;
}

const TextInput = styled.input.attrs<TextInputProps>((props) => ({
  // Статичные свойства
  // Мы можем задать им значение
  type: "text",
  onFocus: () => console.log("Focused"),

  // Динамическая зависимость
  // Можем изменить её перд отправкой в стили
  size: (props.size || 4) + "px",
}))<TextInputProps>`
  padding: ${({ size }) => size};
  margin: ${({ size }) => size};
`;

export const SimpleComponentWithAttrs = () => <TextInput size={8} />;

Лучше не использовать без необходимости статичные свойства внутри стилизованной компоненты, так как зачастую эти свойства задаются в JSX. Просто знайте об этой фиче.

Наследование стилей

Стилизованные компоненты могут наследовать стили других компонент, что помогает избежать дублирования кода.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

const Text = styled.div`
  font-size: 12px;
`

// TomatoText наследует те же стили и тег, что и Text
const TomatoText = styled(Text)`
  color: "#FF6347";
`

export const SimpleComponentWithExtending = () => (
  <>
    <Text>Simple Text</Text>
    <TomatoText>Tomato Text</TomatoText>
  </>
)

CSS-фрагмент

Важной особенностью является передача фрагментов стилей внутрь стилизованной компоненты, данный подход упрощает написание кода и предотвращает повторяемые элементы стилей.

// Какая-тоКомпонента.tsx

import styled, { css } from 'styled-components'

// Создаем css фрагмент
const fontStyles = css`
  font-size: 12px;
  line-height: 14px;
  font-weight: 700;
`

const Text1 = styled.h1`
  color: blue;
  // Дополняем стили Text1 фрагментом fontStyles 
  ${fontStyles}
`
const Text2 = styled.p`
  color: blueviolet;
  ${fontStyles}
`

export const SimpleComponent = () => (
  <>
    <Text1>Some Text</Text1>
    <Text2>Another some text</Text2>
  </>
)

Глобальные стили

Как и подобает любому веб-приложению, добавим нашему проекту главный файл со стилями. Откроем global.ts и с помощью функции createGlobalStyle сформируем компонент с глобальными стилями.

// global.ts

import { createGlobalStyle } from 'styled-components'

export default createGlobalStyle`
  * {
    ...
  }

  *::before,
  *::after {
    ...
  }

  body {
    ...
  }
`

Далее добавим его в приложение.

// App.tsx

import { Routing } from 'routing'

// Импортируем глобальные стили
import GlobalStyles from 'styles/global'

const App = () => {
  return (
    <>
      <Routing />
      { // Добавляем его как тег }
      <GlobalStyles />
    </>
  )
}

export default App

Тема

Как насчёт сделать тему приложения одним большим источником истины в котором мы будем хранить палитру, размеры, медиа запросы и прочие свойства, связанные с компонентами?

В файле theme.ts объявим переменную со всеми необходимыми свойствами.

// theme.ts

export const baseTheme = {
  colors: {
    primary: '#7986cb',
    secondary: '#2b2b2b',
    success: '#4caf50',
    danger: '#f44336 ',
    
    bg: '#E5E4E8',
    font: '#19191B',
  },

  media: {
    extraLarge: '(max-width: 1140px)',
    large: '(max-width: 960px)',
    medium: '(max-width: 720px)',
    small: '(max-width: 540px)',
  },

  // in px
  sizes: {
    header: { height: 56 },
    container: { width: 1200 },
    footer: { height: 128 },
    modal: { width: 540 },
  },

  // in ms
  durations: {
    ms300: 300,
  },

  // z-index
  order: {
    header: 50,
    modal: 100,
  },
}

Данный подход избавляет нас от магических чисел, и даёт возможность применять конкретные значения в стилях и компонентах.

Магические числа в коде — одно из олицетворений зла и лени у программиста. Это целочисленная константа, которая встречается в коде, смысл которой сложно понять.

C этого момента мы уже просто можем импортировать эту константу в необходимых местах и использовать её.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

import { baseTheme } from 'styles/theme'

const StyledHeader = styled.header`
  background-color: ${baseTheme.colors.secondary};
  height: ${baseTheme.sizes.header.height}px;
  z-index: ${baseTheme.order.header};
`

export const Header = () => <StyledHeader>Title</StyledHeader>

Можно передавать тему внутрь стилизованных компонент при помощи ThemeProvider, чтобы постоянно не импортировать наш baseTheme.

// App.tsx

import { ThemeProvider } from 'styled-components'

import { Routing } from 'routing'
import GlobalStyles from 'styles/global'

// Импортируем тему
import { baseTheme } from 'styles/theme'

const App = () => {
  return (
    <ThemeProvider theme={baseTheme}>
      <Routing />
      <GlobalStyles />
    </ThemeProvider>
  )
}

export default App

Теперь в любой стилизованной компоненте, которая находится внутри провайдера, мы имеем доступ к baseTheme.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

const StyledHeader = styled.header`
  // Получаем значение темы внутри стрелочной функции,
  // где деструктурируем props
  background-color: ${({ theme }) => theme.colors.secondary};
  height: ${({ theme }) => theme.sizes.header.height}px;
  z-index: ${({ theme }) => theme.order.header};
`

export const Header = () => <StyledHeader>Title</StyledHeader>

У этой реализации есть одна небольшая проблема — редактор кода не даёт подсказок при написании свойств theme. Для её решения нам нужно типизировать тему и расширить интерфейс DefaultTheme.

В корне src создаём директорию interfaces с файлом styled.ts, где опишем каждое свойство темы.

// styled.ts

export interface ITheme {
  colors: {
    primary: string
    secondary: string
    success: string
    danger: string
    
    bg: string,
    font: string,
  }

  media: {
    extraLarge: string
    large: string
    medium: string
    small: string
  }

  sizes: {
    header: { height: number }
    container: { width: number }
    footer: { height: number }
    modal: { width: number }
  }

  durations: {
    ms300: number
  }

  order: {
    header: number
    modal: number
  },
}

После этого создаём в корне src файл styled.d.ts, где расширяем интерфейс стандартной темы с помощью нашего типа.

// styled.d.ts
import 'styled-components';

import { ITheme } from 'interfaces/styled';

declare module 'styled-components' {
  export interface DefaultTheme extends ITheme {}
}

d.ts файлы — описывают форму сторонней библиотеки и позволяют компилятору TypeScript знать, как обращаться с этим сторонним кодом.

Не забудем дописать интерфейс ITheme нашей baseTheme, чтобы все её значения были корректны.

// theme.ts

import { ITheme } from 'interfaces/styled'

export const baseTheme: ITheme  = {
  // ...
}

Готово!

Если что, мы можем брать этот интерфейс прямо из styled-components, если это будет необходимо.

import { DefaultTheme } from 'styled-components'

Динамическая тема

Если мы хотим создать динамическую тему, например для переключения светлой темы на тёмную — то нам потребуется уже знакомый ThemeProvider и любой стейт менеджер для инициализации и контроля темы.

Для начала создадим enum для определения типа нашей темы в папке interfaces.

// styled.ts

export enum ThemeEnum  {
  light = "light",
  dark = "dark"
}

export interface ITheme {
  // ...
}

Enum — это конструкция, состоящая из набора именованных констант, называемого списком перечисления и определяемого такими примитивными типами, как number и string.

Дополним DefualtTheme в d.ts файле.

// styled.d.ts

import 'styled-components';
import { ITheme, ThemeEnum } from 'interfaces/styled';

declare module 'styled-components' {
  export interface DefaultTheme extends ITheme {
    type: ThemeEnum
  }
}

Создадим тёмную и светлую тему на основе baseTheme. Цвета bg и font — динамические, они будут изменяться при переключении темы.

// theme.ts

import { DefaultTheme } from 'styled-components'
import { ITheme, ThemeEnum } from 'interfaces/styled'

// ITheme - используется для статичной темы
const baseTheme: ITheme = {
  // ...
}

// DefaultTheme - используется для динамических тем 
export const lightTheme: DefaultTheme = {
  ...baseTheme,
  type: ThemeEnum.light,

  colors: {
    ...baseTheme.colors,
    bg: '#E5E4E8',
    font: '#19191B',
  },
}

export const darkTheme: DefaultTheme = {
  ...baseTheme,
  type: ThemeEnum.dark,

  colors: {
    ...baseTheme.colors,
    bg: '#19191B',
    font: '#E5E4E8',
  },
}

Ниже приведён пример инициализации и переключения темы в MobX.

// ui.ts - одно из хранилищ MobX

import { makeAutoObservable } from 'mobx'
import { DefaultTheme } from 'styled-components'

import { ThemeEnum } from 'interfaces/styled'
import { darkTheme, lightTheme } from 'styles/theme'


export class UIStore {
  theme: DefaultTheme = lightTheme
  
  constructor() {
    makeAutoObservable(this)
  }

  get isLightTheme() {
    return this.theme.type === ThemeEnum.light
  }

  // Переключатель темы
  toggleTheme() {
    this.theme = this.isLightTheme ? darkTheme : lightTheme
  }
}

Передаём тему из стейт менеджера в ThemeProvider.

// App.tsx

...

  return (
    <ThemeProvider theme={uiStore.theme}>
      <Routing />
      <GlobalStyles />
    </ThemeProvider>
  )
  
...

После этого реализовываем механизм переключения темы, присваиваем динамические цвета в нужных местах и по желанию можно добавить плавность анимации.

Микрокомпоненты

Это простые переиспользуемые стилизованные компоненты, состоящие из одного узла, которые часто встречаются в приложении. Это могут быть заголовки, тексты, контейнеры, иконки и другие.

Перейдём в файл components.ts и создадим несколько подобных компонент.

// components.ts

// Пример вертикального разделителя
interface DividerProps {
  height?: number
  heightMob?: number
}
export const Divider = styled.div<DividerProps>`
  height: ${({ height = 8 }) => height}px;

  // Медиа запрос
  @media ${({ theme }) => theme.media.large} {
    height: ${({ heightMob = 4 }) => heightMob}px;
  }
`

// Пример заголовков разного уровня
interface TitleProps {
  weight?: 200 | 300 | 400 | 500 | 600 | 700
}

export const Title1 = styled.h1<TitleProps>`
  font-size: 24px;
  font-weight: ${({ weight = 700 }) => weight};
`

export const Title2 = styled.h2<TitleProps>`
  font-size: 18px;
  font-weight: ${({ weight = 700 }) => weight};
`

Приведу пример использования.

// Какая-тоКомпонента.tsx

import { Divider, Title1, Title2 } from 'styles/components' 

export SimpleComponent = () => (
  <div>
    <Title1>Some title H1</Title1>
    <Divider height={16}/>
    <Title2 weight={200}>Some title H2</Title2>
  </div>
)

Анимации

Для этого в Styled Components есть специальная функция keyframes внутрь которой мы передаём ключевые кадры. Их написание полностью схоже с аналогичным в CSS. Все анимации можно записывать в отдельный файл, так как теперь мы можем хранить значения в переменной.

// animations.ts

import { keyframes } from 'styled-components'

export const spin = keyframes`
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
`

Применим её к одной из микрокомпонент.

// components.ts

import styled, { css } from 'styled-components'

// Импортируем компоненту FontAwesomeIcon и её пропсы,
// на базе которой мы расширим интерфейс передаваемых данных
import {
  FontAwesomeIcon,
  FontAwesomeIconProps,
} from '@fortawesome/react-fontawesome'

// Импорт ключевых кадров
import { spin } from './animations'

interface FAIconProps extends FontAwesomeIconProps {
  // Временный атрибут
  $animated?: boolean // не будет передан в FontAwesomeIcon
}
export const FAIcon = styled(FontAwesomeIcon)<FAIconProps>`
  ${({ $animated }) =>
    $animated
      ? css`
          animation: ${spin} 4s infinite linear;
        `
      : css`
          animation: none;
        `}
`

Временный атрибут — обозначается префиксом $. Предотвращает дальнейшее прокидывание пропса в стилизуемый компонент.

Пример использования микрокомпоненты FAIcon.

// Какая-тоКомпонента.tsx

import { faCog } from '@fortawesome/free-solid-svg-icons'
import { faReact } from '@fortawesome/free-brands-svg-icons'

import { FAIcon } from 'styles/components'

export const SimpleComponent = () => (
  <>
    <FAIcon icon={faCog} color="#a8324a" $animated/>
    <FAIcon icon={faReact} color="#3265a8"/>
  </>
)

Импорты

Совместное размещение стилизованных компонент с вашими фактическими компонентами упрощает файловую структуру вашего проекта. И для улучшения читаемости кода — перенесём стили после основного кода.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

// Импорт микрокомпоненты
import { Title1 } from 'styles/components'

export const Header = () => (
  <StyledHeader>
    <Title1>Some Title!</Title1>
  </StyledHeader>
)

const StyledHeader = styled.header`
  background-color: ${({ theme }) => theme.colors.secondary};
  padding: 0 16px;
`

Если же компонента разрослась до более крупных масштабов, то будет правильней вынести стили в отдельный файл styles.ts рядом с компонентой.

// Какая-тоКомпонента.tsx

import { faSun } from '@fortawesome/free-regular-svg-icons'

import { StyledHeader, HeaderTitle } from './styles'
import { Title1, SupText, FAIcon } from 'styles/components'
import { Button } from 'components/Button'

export const Header = () => (
  <StyledHeader>
    <HeaderTitle>
      <Title1 weight={200}>Plankton</Title1>
      <SupText>React + Mobx + SC</SupText>
    </HeaderTitle>
    <Button
      variant={Button.variant.ghost}
      color={Button.color.secondary}
      size={Button.size.lg}
    >
      <FAIcon
        color={'#c7a716'}
        icon={faSun}
      />
    </Button>
  </StyledHeader>
)

Ещё одна хорошая практика — это компактный импорт стилизованных компонент.

// Какая-тоКомпонента.tsx

import { faSun } from '@fortawesome/free-regular-svg-icons'

// Импортируем всё из файла styles.
import * as S from './styles'
import * as C from 'styles/components'
import { Button } from 'components/Button'

export const Header = () => (
  <S.Header>
    <S.HeaderTitle>
      <C.Title1 weight={200}>Plankton</Title1>
      <C.SupText>React + Mobx + SC</SupText>
    </S.HeaderTitle>
    <Button
      variant={Button.variant.ghost}
      color={Button.color.secondary}
      size={Button.size.lg}
    >
      <C.FAIcon
        color={'#c7a716'}
        icon={faSun}
      />
    </Button>
  </S.Header>
)

Такой подход даёт нам несколько преимуществ:

  1. Не засоряем код лишними импортами.

  2. Логическое разделение. У каждой стилизованной компоненты теперь есть своя приставка, что упрощается ориентирование в коде. В моём случае:
    Без приставки — обычные компоненты.
    S — Стили для нашей фактической компоненты.
    C — Микрокомпоненты.

  3. Коллизия названий. Часто основную обёртку обзываю StyledЧто-тоТам, теперь мы спокойно можем писать S.Что-тоТам. Это связано с повторяющимся названием родительской компоненты и стилизованной обертки.

Вариации

При создании компоненты мы часто пытаемся интерпретировать её в разных видах, сохраняя основной функционал, будь-то кнопки, текстовые поля, карточки и другие.

Разберём эту практику на примере кнопки, которая может быть разных размеров.

// Где-тоВКакой-тоКомпоненте.tsx

import { Button } from 'components/Button'

...

<Button size={Button.size.lg}>
  Text
</Button>

...
// Button.tsx

import { PropsWithChildren } from 'react'

import * as S from './styles'

// Размеры кнопок
export enum ButtonSize {
  xs = 'xs',
  sm = 'sm',
  md = 'md',
  lg = 'lg',
}

// Интерфейс входящих зависимостей
export interface ButtonProps {
  size?: ButtonSize
}

const ButtonComponent = ({
  children,

  // Так как size опциональный, зададим ему значение по умолчанию
  size = ButtonSize.md,
}: PropsWithChildren<ButtonProps>) => {
  return (
    <S.Button size={size}>
      <span>{children}</span>
    </S.Button>
  )
}

// Присваиваем Enum ButtonSize компоненте 
ButtonComponent.size = ButtonSize

export const Button = ButtonComponent

Я не буду сильно заострять внимание на реализации компоненты, так как это другая тема. Здесь стоит понимать, что значение size может задаваться только в рамках enum ButtonSize.

Перейдём к стилям.

// styles.tsx

import styled, { css } from 'styled-components'

// Импортируем enum
import { ButtonSize } from '.'

interface ButtonProps {
  size: ButtonSize
}

export const Button = styled.button<ButtonProps>`
  // ...
  
  ${({ size }) => 
    size === ButtonSize.lg
      ? css`
          height:48px;
          font-size: 18px;
      `
      : size === ButtonSize.md
      ? css`
          height: 40px;
          font-size: 16px;
      `
      : size === ButtonSize.sm
      ? css`
          height: 32px;
          font-size: 14px;
      `
      : css`
          height: 24px;
          font-size: 12px;
      `
  }
`

Думаю вы согласитесь, что это не самое красивое решение. Поэтому предлагаю такую альтернативу: избавимся от тернарных операторов и создадим объект sizes из которого просто будем брать необходимое значение по ключу.

Получим следующий результат.

// styles.tsx

import styled, { css } from 'styled-components'

import { ButtonSize } from '.'
import { StyledVariants } from 'interfaces/styled'

interface ButtonProps {
  size: ButtonSize
}

export const Button = styled.button<ButtonProps>`
  // ...
  
  ${({ size }) => sizes[size]}
`

const sizes: StyledVariants<ButtonSize> = {
  xs: css`
    height: 24px;
    font-size: 12px;
  `,
  sm: css`
    height: 32px;
    font-size: 14px;
  `,
  md: css`
    height: 40px;
    font-size: 16px;
  `
  lg: css`
    height: 48px;
    font-size: 18px;
  `
}

Необходимо типизировать данный объект, чтобы быть уверенными в задаваемых свойствах. Дополним наш файл с интерфейсами.

// styled.ts

// тип css фрагмента
import { FlattenSimpleInterpolation } from 'styled-components'

// E - элемент enum
export type StyledVariants<E extends string | number> = {
  [key in E]?: FlattenSimpleInterpolation
}

Что если вариация зависит не от одного значения, а от двух или более? Я добавлю в компонент кнопки еще две зависимости variant и color.

// Button.tsx

...

export enum ButtonVariant {
  solid = 'solid',
  outline = 'outline',
  ghost = 'ghost',
}

export enum ButtonColor {
  primary = 'primary',
  secondary = 'secondary',
  success = 'success',
  danger = 'danger',
}

...

Для работы с несколькими параметрами нам потребуется создать конструкцию switch case, которая будет возвращать один из CSS-фрагментов определенной вариации и цвета.

// styles.tsx

import styled, { css } from 'styled-components'

import { ButtonVariant, ButtonColor } from '.'

interface ButtonProps {
  variant: ButtonVariant
  color: ButtonColor
}

export const Button = styled.button<ButtonProps>`
  // ...
  
    ${({ 
      variant = ButtonVariant.solid,
      color = ButtonColor.primary,
      theme
    }) => {
    const themeColor = theme.colors[color]

    switch (variant) {
      case ButtonVariant.solid:
        return css`
          background-color: ${themeColor};
        `
      case ButtonVariant.outline:
        return css`
          background-color: transparent;
          color: ${themeColor};
          border: 1px solid ${themeColor};
        `
      case ButtonVariant.ghost:
        return css`
          background-color: transparent;
          color: ${themeColor};
          border: none;
          box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px;
        `
    }
  }}
`

Бонус

polished

Хорошее дополнение для Styled Components о котором вы должны знать. Этот пакет даёт массу новых возможностей, например затемнять и осветлять цвета, переводить hex в rgb, делать элемент прозрачным с привязкой к определённому цвету и многое другое.

Оставим эту библиотеку с закосом на сиквел.

https://www.npmjs.com/package/polished

Цветные скобки

В процессе использования Styled Components, я столкнулся с одной неприятной багой плагина Bracket Pair Colorizer. Так как мы пишем стили внутри шаблонных строк, то зачастую скобки разных уровней неправильно подсвечиваются. Благо решение есть, причем очень свежее (на данный момент), с недавним обновлением VSCode ввёл свои цветные скобки, и как пишут сами разработчики, их реализация в 10 000 раз быстрее плагина.

Нужно просто добавить следующий параметр в настройки вашего редактора:

"editor.bracketPairColorization.enabled": true

https://code.visualstudio.com/blogs/2021/09/29/bracket-pair-colorization

getTransitions

Хочу поделиться своим хелпером — эта функция упростит написание транзишенов, особенно в тех местах, где мы хотим реализовать смену темы.

import { css } from 'styled-components'

export const getTransition = (
  duration: number,
  property: string[] | string = ['background-color', 'color'],
  animation = 'ease'
) =>
  css`
    transition-property: ${Array.isArray(property)
      ? property.join(', ')
      : property};
    transition-duration: ${duration}ms;
    transition-timing-function: ${animation};
  `

Пример использования в стилизованной компоненте.

const Something = styled.div`
  ${({ theme }) =>
    getTransition(theme.durations.ms300, [
      'background-color',
      'border-color',
      'color',
   ])}
`

Больше примеров

В данном репозитории собраны готовые компоненты, статьи, видео, проекты, созданные на базе Styled Components, и многое-многое другое. Советую посмотреть.

https://github.com/styled-components/awesome-styled-components#components

Заключение

Надеюсь я смог донести уникальность и гибкость написания CSS-кода с помощью Styled Components. Как по мне, это идеальный инструмент для React проектов, где мы можем позволить себе инновации, уникальные подходы и массу вариативности!

Если есть желание посмотреть на эти практики в деле, то вот исходники и демо проекта.

Код
Демо клиент
Демо storybook

Комментарии (13)


  1. markelov69
    25.11.2021 09:02
    +12

    Для работы в CSS нужно использовать SCSS и CSS Modules, а CSS in JS это мракобесие и извращенство засоряющее код, добавляющее ненужный шум и размывающее явное визуальное отличие компонентов в JSX'e от обычных div'ов, span'ов и т.п.


    1. DarthVictor
      26.11.2021 12:11

      Вообще есть нормальные compile time реализации CSS in JS.


      1. markelov69
        26.11.2021 12:54

        Вообще есть нормальные compile time реализации CSS in JS.

        Это не имеет отношения к тому что я написал выше. Вы скинули только то, что на выходе при сборке эти стили извлекутся в отдельный css файл. Все остальные жирные минусы никуда не делись. И исходный код благодаря этому подходу как был ужасным, так и остался.


        1. DarthVictor
          26.11.2021 17:55
          +1

          Вы скинули только то, что на выходе при сборке эти стили извлекутся в отдельный css файл.

          Всего-то убирает весь оверхед css-in-js решений.

          Все остальные жирные минусы никуда не делись.

          А какие именно? Если про

          размывающее явное визуальное отличие компонентов в JSX'e от обычных div'ов, span'ов и т.п.

          то мне это тоже не нравится. Но у linaria этот функционал даже не в базовой библиотеке, а плагином. Ничего не мешает тупо не ставить этот пакет.

          При этом сохраняется главное преимущество css-in-js решений: неиспользуемые классы сразу видны.


  1. jodaka
    25.11.2021 12:08
    +2

    У styled проблемы с производительностью. Как только в DOM появляется пара десятков тысяч элементов, styled начинает визуально тормозить, когда пользователь покидает страницу и нужно почистить стили для всех этих десятков тысяч элементов.

    Соответственно, если какие-то сложные гриды с кучей styled компонентов внутри — жди беды. Наступили на грабли уже и с ant-table и c aggrid.


    1. andy128k
      25.11.2021 12:31
      -2

      Разве для тех же CSS-модулей ситуация не такая же?


      1. markelov69
        25.11.2021 13:22
        +3

        Это же webpack css-loader, флаг modules: true и получаем css modules. Т.е. это просто обычный css + имена классов изменяются чтобы можно было во многих компонентах использовать .title и т.п.


        https://webpack.js.org/loaders/css-loader/#modules


        https://github.com/css-modules/css-modules


        В общем вот живой пример:
        https://codesandbox.io/s/jolly-tdd-jyh2w?file=/src/index.js


        Смотрите там в консоль и сразу все увидите.


        1. andy128k
          25.11.2021 15:09

          Ну а разве styled components делает что-то другое? Вроде как он тоже генерирует CSS и классы для селекторов, и так же генерирует `<style/>` узел в `<head/>`. Ну разве что в случае модулей это будет `<link/>`. Но в обоих случаях браузеру нужно загружать все стили.


          1. markelov69
            25.11.2021 15:59

            Нет, он не тоже самое делает. Всё это хранится в JS файле бандла, а не вырезано отдельным .css файлом сбилженым, отсюда неоправданно более жирный JS бандл, и при каждом изменении JS'a вынужденно пользователь загружает ещё CSS in JS месте с ним, хотя мог бы и не загружать без изменения стилей и пользовался бы закэшированным .css'ником. В любом случае css in js это просто неоправданно срать в коде и срать в бандле итоговом.


            1. andy128k
              25.11.2021 20:46
              -1

              Чтобы CSS был отдельным файлом тот же webpack нужно отдельно настраивать. По-умолчанию стандартная связка style-loader/css-loader помещают стили в JS бандл.

              > css in js это просто неоправданно срать в коде

              Когда-то так же говорили про JSX, якобы это "html in js"

              > срать в бандле итоговом.

              Не уверен что это недостаток. С помощью того же url-loader картинки бандлят. Иногда может быть удобно иметь приложение в виде одного файла, например для встраивания.

              Не знаю деталей про styled components, но вроде бы есть аналогичные библиотеки которые позволяют извлечь CSS (google подсказывает linaria и astroturf, может есть и другие). Так что я бы не стал обобщать на весь "css in js".


              1. markelov69
                25.11.2021 21:12
                +2

                Чтобы CSS был отдельным файлом тот же webpack нужно отдельно настраивать. По-умолчанию стандартная связка style-loader/css-loader помещают стили в JS бандл.

                Да, надо настроить, но никто в здравом уме не использует голый webpack без кастомного конфига и у всех уважающих себя разработчиков есть webpack config для всех проектов, который уже можно дотюнить под конкретные нужны если в этом есть нужда. Есть ещё конечно и отдельные индивидумы которые используют Create React App не в hello world'e, а в комерческих проектах это конечно отдельная смешная история, но чтож и такие к сожалению тоже есть)


                Не знаю деталей про styled components, но вроде бы есть аналогичные библиотеки которые позволяют извлечь CSS (google подсказывает linaria и astroturf, может есть и другие). Так что я бы не стал обобщать на весь "css in js".

                Тогда полностью его "преимущества" улетучиваются и его использование становится абсурдным в квадрате.


                Когда-то так же говорили про JSX, якобы это "html in js"

                Тут другая история, неравнозначная аналогия, но попытка не пытка конечно)


                Если хотите используйте CSS in JS сколько влезет, только не в коммерческих проектах пожалуйста, т.к. потом кому-то кроме вас их поддерживать придется. Вы же уйдете и забудете это как страшный сон, а другим потом волосы на голове рвать или переписывать все.


      1. alexesDev
        29.11.2021 14:25

        В v3 половина просадки производительности была из-за поддержки тем, которыми большая часть проектов не пользуется, а просадка есть. Это так, к примеру. Текущее состояние не знаю. Те нет, не тоже самое он делает.


    1. justboris
      25.11.2021 15:06

      Есть styled-components как паттерн, функция styled`...`, и у неё есть разные реализации, и styled-components тут не очень. Есть варианты получше, посмотрите на emotion или jss