Итак, Styled Components. Буквально – стилизованные компоненты. Собственно, в этом вся суть и статью можно завершать.
Шучу. Продолжаем.
TL;DR#
const sharedText = css`
  color: white;
  margin: 1rem;
`;
const Title = styled.h1`
  ${sharedText};
  font-size: 2rem;
`;
const Text = styled.p`
  ${sharedText};
  font-size: 1rem;
`;
const TextBase = styled.p`
  font-size: 1rem;
  text-align: center;
`;
const BlueText = styled(TextBase)`
  color: blue;
`;
const PinkText = styled(TextBase)`
  color: pink;
`;
const Container = styled.div`
  margin: ${({ isLarge }) => (isLarge ? '5rem' : '1rem')};
`;
const TextBase = styled.p`
  font-size: 1rem;
  text-align: center;
  color: ${(props) => props.color};
`;
const MyComponent = function () {
  return (
    <Container isLarge>
      <TextBase color="#0000FF">I'm BlueTextBase>
      <TextBase color="pink">I'm PinkTextBase>
    Container>
  );
};Поехали#
Давайте напишем простенький React-компонент, для упрощения процесса я буду использовать SCSS (ведь его все любят):
.hello {
  color: white;
  background-color: cyan;
  &:hover {
    background-color: black;
  }
}И JSX:
function HelloClassNameComponent() {
  return <div className="hello">Hello Class Name!div>;
}Теперь то же самое, но при помощи Styled Components:
import styled from 'styled-components';
const Div = styled.div`
  color: white;
  background-color: red;
  &:hover {
    background-color: black;
  }
`;
function HelloStyledComponent() {
  return <Div>Hello Styled!Div>;
}Сразу видна проблема с подсветкой кода, но это уже по моей вине: я пока не очень разобрался с настройками Prism в Eleventy. Просто имейте это в виду.
Выглядит весьма похоже. Используя свежие фишки JavaScript под названием шаблонные строки и теговые шаблоны Styled Components буквально позволяет писать привычный CSS прямо в JS-модулях и таким образом теперь ваш компонент целиком и полностью отвечает не только за свою структуру и визуальную логику, но и за внешнее оформление тоже.
И привычный и такой любимый родительский селектор (&, амперсанд) имеется.
Песочница#
Установка и настройка Styled Components будет описана в следующей статье цикла, поскольку нюансов и фишек слишком много. Здесь я познакомлю вас с синтаксисом и основными приёмами работы. Чтобы было проще стартовать, я подготовил песочницу на CodePen, в рамках которой можно просто запускать код из статьи. Да, я ленивый: остальные примеры будут даны просто текстом.
See the Pen Unknown Pen on CodePen.
Прокидываем props'ы#
Продолжаем.
Одного только исчезновения className явно недостаточно чтобы набрать новых сторонников. Усложняем:
import styled from 'styled-components';
const Div = styled.div`
  color: ${(props) => props.color || 'white'};
  background-color: cyan;
  &:hover {
    background-color: black;
  }
`;
function HelloStyledComponent() {
  return <Div color="green">Hello Styled!Div>;
}Что мы сделали? Мы передали цвет текста через свойства компонента (properties, props... пробросили через пропсы, в общем). Ничего не передаём – получаем красный, вот так просто. Теперь можно поступить вот так:
const colors = ['red', 'green', 'blue'];
function HelloPropsComponent() {
  return (
    <article>
      {colors.map((color) => (
        <Div color={color}>Hello Styled!Div>
      ))}
    article>
  );
}Получим три компонента с разным цветом текста, но одинаковыми остальными свойствами и реакцией на наведение мыши. Чтобы повторить подобное на классическом CSS без привлечения сторонних инструментов, пришлось бы сначала определить классы для разных цветов, а после воспользоваться или библиотекой classnames или склеивать строки названий классов вручную:
.hello {
  color: white;
  background-color: cyan;
  &:hover {
    background-color: black;
  }
  &--red {
    color: red;
  }
  &--green {
    color: green;
  }
  &--blue {
    color: blue;
  }
}const colors = ['red', 'green', 'blue'];
function HelloClassComponent() {
  return (
    <article>
      {colors.map((color) => {
        return <div className={`hello hello--${color}`}>Hello Class Name!div>;
      })}
    article>
  );
}Да, можно попробовать пользовательские свойства CSS (переменные, Custom Properties), будет гораздо гибче. Но при работе «в лоб» всё ещё достаточно неудобно.
Задаём свойство:
.hello {
  color: var(--color);
  background-color: cyan;
  &:hover {
    background-color: black;
  }
}И меняем в нужный нам момент:
const colors = ['red', 'green', 'blue'];
function HelloVarComponent() {
  return (
    <article>
      {colors.map((color) => {
        const colorStyle = { '--color': color };
        return (
          <div className="hello" style={colorStyle}>
            Hello Custom Properties!
          div>
        );
      })}
    article>
  );
}Впрочем, в комбинации со Styled Components пользовательские свойства могут раскрыться в полной мере и дать неограниченный простор для темизации.
Чтобы не навлечь на себя гнев апологетов CSS-подхода я сразу должен отметить: написанный вручную CSS изначально является более эффективным. Styled Components (и Emotion, например) не умеют объединять похожие классы самостоятельно. Т. е. на каждый div будет сгенерирован свой класс, с одинаковым фоном и :hover-ом. Кто-то вполне имеет право поморщиться от такого подхода.
Но это автоматизируется и давайте будем честны: в CSS мы всё же создали три разных класса, а в SC – нет. И это можно будет исправить, расскажу чуть далее.
Переиспользование и css``#
Ладно одно свойство, а если у вас их с десяток? А если вам поддержка темизации нужна? А если это свойство должно полностью преобразить компонент? Разным ситуациям – разный внешний вид по переданному одному лишь параметру. Введём ещё одну функцию, css.
Название говорит само за себя:
import styled, { css } from 'styled-components';
const flexStyles = css`
  display: flex;
  flex-direction: column;
  align-items: space-around;
  justify-content: center;
  color: ${({ color }) => color || 'red'};
`;
const blockStyles = css`
  display: block;
`;
const Div = styled.div`
  ${({ isAlt }) => {
    isAlt ? blockStyles : flexStyles;
  }}
`;
function HelloCssComponent() {
  return (
    <Div isAlt color="green">
      Hello Styled!
    Div>
  );
}Мы получили независимый переиспользуемый блок стилей и вынесли описание представления за пределы минимальной, но всё же логики блока. И таких комбинаций свойств и стилей может быть множество даже в пределах одного элемента. А ещё можно и просто вот так:
const sharedStyles = css`
  color: white;
  margin: 1rem;
`;
const Title = styled.h1`
  ${sharedStyles};
  font-size: 2rem;
`;
const Text = styled.p`
  ${sharedStyles};
  font-size: 1rem;
`;Конструкция, напомню, называется теговым шаблоном. Я писал выше, что библиотека Styled Components на них основана. Сама директива styled – фабрика этих самых теговых шаблонов и это первая подсказка к самой крутой фишке библиотеки.
Адаптивная вёрстка и media``#
Как и в SCSS, вы просто описываете все правила непосредственно в описании стилизованного компонента:
import styled from 'styled-components';
const size = {
  mobileS: '320px',
  mobileM: '375px',
  mobileL: '425px',
  tablet: '768px',
  laptop: '1024px',
  laptopL: '1440px',
  desktop: '2560px',
};
const device = {
  mobileS: `(min-width: ${size.mobileS})`,
  mobileM: `(min-width: ${size.mobileM})`,
  mobileL: `(min-width: ${size.mobileL})`,
  tablet: `(min-width: ${size.tablet})`,
  laptop: `(min-width: ${size.laptop})`,
  laptopL: `(min-width: ${size.laptopL})`,
  desktop: `(min-width: ${size.desktop})`,
  desktopL: `(min-width: ${size.desktop})`,
};
const Div = styled.div`
  color: white;
  background-color: red;
  &:hover {
    background-color: black;
  }
  @media ${device.mobileS} {
    color: green;
  }
`;
function HelloMediaComponent() {
  return <Div>Hello Media!Div>;
}Мы создали говорящие за себя константы со списками разрешений экранов и определили под них устройства. Они могут быть экспортированы из любого места в проекте и, соответственно, использованы где угодно. Теперь вы знаете достаточно чтобы начать применять SC в реальных проектах.
Композиция#
«Подожди, дядя, – скажете вы. – У нас тут в CSS есть композиция классов, а с SCSS это вообще в культ возведено и композицию классов мы определяем вложенностью. Амперсанд наше всё!», – и будете правы в своём вопросе. Но, кажется, вы забыли, что цель CSS-in-JS в итоге – создать всё те же классы. И вот здесь, внезапно, композиция классов на наших глазах превращается в композицию компонентов:
import styled from 'styled-components';
import icon from './icon.png';
const Icon = styled.i`
  display: block;
  width: 16px;
  height: 16px;
  background: trasnsparent url(${icon}) center/contain no-repeat;
`;
const Button = styled.button`
  background: none;
  border: 1px cyan solid;
  ${Icon} {
    display: inline-block;
    width: 12px;
    height: 12px;
  }
`;Можете прийти в @htmlshitchat и рассказать мне про использование тега i для иконки, но... суть в том, что мы получили такую желанную и привычную композицию классов, да ещё и в виде всеми любимой вложенности. Родительский селектор тоже работает как надо:
import styled from 'styled-components';
import {Menu} from ‘../Menu’;
import {Dropdown} from ‘../Dropdown’;
const Button = styled.button`
  border: 1px solid darkgreen;
  background: darkkhaki;
  ${Menu} & {
    border: none;
    background: transparent;
  }
  ${Dropdown} & {
    text-indent: -9999px;
    &::after {
      // create some arrow maybe
    }
}`;Что произошло? Мы использовали родительский селектор (в нашем случае компонент, Button) и заставили его выглядеть иначе при использовании внутри компонентов Menu и Dropdown. Вот так вот просто.
Вот только я сразу хочу предупредить, что так пишут довольно редко: это нарушает принцип единственной ответственности (насколько он вообще применим для компонентов). Гораздо чаще делают следующее:
import styled from 'styled-components';
import Icon from '@/ui/Icon';
const ButtonIcon = styled(Icon)`
  display: inline-block;
  width: 12px;
  height: 12px;
`;То есть мы просто передаём в styled ваш компонент в качестве аргумента и получаем теговый шаблон, который генерирует новый класс на основе уже существующего класса, принадлежащего стилизуемому элементу, и новых передаваемых стилей. Магическим образом рождается новый компонент. Мне кажется, на этом месте у самых внимательных должна щёлкнуть в голове весьма очевидная идея: «Раз Styled Components заведует классами, можно ли подмешать их к обычным компонентам?».
И ответ – да!
До тех пор пока ваши компоненты могут принимать класс (className) через переданные свойства (props) – SC может собрать композицию! В таком случае я придерживаюсь именования из документации: добавляю префикс Styled к имени компонента. Это позволяет избежать любых неоднозначностей и сложностей в именовании.
Следующий пример взят из документации с изменениями. Я вообще крайне рекомендую её прочесть, если владеете английским. Интерактивные примеры там – шик, пройдите и попробуйте.
import styled from 'styled-components';
const Link = ({ className, children }) => (
  <a href="/" className={className}>
    {children}
  a>
);
const StyledLink = styled(Link)`
  color: palevioletred;
  font-weight: bold;
`;
const Styled2Link = styled(StyledLink)`
  color: red;
`;
export default function App() {
  return (
    <div>
      <Link>Unstyled, boring LinkLink>
      <br />
      <StyledLink>Styled, exciting LinkStyledLink>
      <br />
      <Styled2Link>Styled, exciting Link 2Styled2Link>
    div>
  );
}И вот теперь обратите внимание. Помните я сказал, что Styled Components из коробки не умеет объединять ваши CSS-правила? Так вот в данном примере вы, фактически, объединили их сами. Как и в классическом подходе:

Магия#
Напоследок немного обещанной магии. Возьмём популярный фреймворк Material UI и обратим его на свою сторону. Компоненты MUI принимают классы? Значит, принимают и правила SC.
See the Pen Unknown Pen on CodePen.
Есть только одна хитрая проблема. Вы же не забыли, что правила CSS применяются по-порядку? Нам нужно быть уверенными, что стили SC загрузятся после стилей JSS (который применяется в MUI по-умолчанию). Вот для этого и нужен провайдер контекста стилей StylesProvider. Он гарантирует, что правила JSS будут загружены первыми. А SC, соответственно, уже после них. Ну ещё есть хак с &&. Догадайтесь уже сами, что он сделает.
Полезно и познавательно#
На этом наш краткий экскурс в Styled Components закончен. В следующих статьях я разберу установку, работу с анимациями, темами и Typescript. А пока рекомендую ознакомиться со следующими ресурсами:
- https://styled-components.com/
 - https://emotion.sh/
 - https://compiledcssinjs.com/
 - https://linaria.dev/
 - https://www.joshwcomeau.com/css/styled-components/
 - https://medium.com/swlh/creating-react-styled-components-with-dynamic-tags-and-props-ef965c839e64
 - https://www.reddit.com/r/reactjs/comments/l4o5k5/the_styledcomponents_happy_path/
 - https://jsramblings.com/how-to-use-media-queries-with-styled-components/
 - https://mxstbr.blog/2016/11/styled-components-magic-explained/