Как мы отказались от использования Styled-System для создания компонентов и изобрели собственный велосипед

    Всем привет! Меня зовут Саша, я сооснователь и по совместительству главный разработчик в Quarkly. В этой заметке я хочу рассказать о том, как концепция атомарного CSS, которой мы придерживаемся, вкупе с недостатками функционала Styled-System (и Rebass, как частного случая использования этой библиотеки) сподвигли нас к созданию своего собственного инструмента, который мы назвали Atomize.

    Небольшая преамбула. Наш проект Quarkly — это микс графического редактора (вроде Figma, Sketch) и конструктора сайтов (по типу Webflow) с добавлением функционала, присущего классическим IDE. Про Quarkly мы обязательно напишем отдельный пост, там есть про что рассказать и что показать, ну а сегодня речь пойдет про упомянутый выше Atomize.

    Atomize лежит в основе всего проекта и позволяет нам решать задачи, которые было бы невозможно или трудно решить с помощью Styled-System и Rebass. Как минимум, решение было бы гораздо менее изящным.

    Если мало времени, чтобы осилить весь пост сейчас, то более лаконично ознакомиться с Atomize можно у нас на GitHub.

    А чтобы знакомство было приятнее, мы запускаем конкурс по сборке react-компонентов с использованием Atomize. Подробнее об этом в конце поста.

    С чего всё началось


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

    Задача не инновационная и, на первый взгляд, вполне решаемая с помощью Styled-System и Rebass. Но этой функциональности нам оказалось недостаточно, а кроме того мы столкнулись со следующими проблемами:

    • неудобная работа с брейкпоинтами;
    • отсутствие возможности писать стили на состояние hover, focus etc;
    • механизм работы с темами показался нам недостаточно гибким.

    Что представляет собой Atomize (кратко)


    image

    Из ключевых особенностей Atomize мы можем выделить следующие:

    • возможность использования переменных из темы в составных css-свойствах;
    • поддержка hover и любых других псевдоклассов;
    • короткие алиасы на каждое свойство (как в emmet);
    • возможность указывать стили на конкретный брейкпоинт, сохраняя при этом читаемость разметки;
    • минималистичный интерфейс.

    При этом у Atomize есть два основных предназначения:

    • создание компонентов с поддержкой атомарного CSS и тем;
    • создание виджетов для интерактивного редактирования в проекте Quarkly.

    Atomize, инструкция по применению


    Перед началом работы необходимо установить зависимости:

    npm i react react-dom styled-components @quarkly/atomize @quarkly/theme

    Atomize является оберткой вокруг styled-component и имеет похожий API. Достаточно вызвать метод с именем необходимого элемента:

    import atomize from '@quarkly/atomize';
     
    const MyBox = atomize.div();

    На выходе мы получаем react компонент, способный принимать любые CSS в виде пропсов.
    Для удобства использования была разработана система алиасов свойств. К примеру bgc === backgroundColor

    ReactDOM.render(<MyBox bgc="red" />, root);

    С полным списком свойств и алиасов можно ознакомиться здесь.

    Также предусмотрен механизм наследования в React:

    const MySuperComponent = ({ className }) => {
       // some logic here
       return <div className={className} />;
    };
     
    const MyWrappedComponent = atomize(MySuperComponent);

    Работа с темами


    Про это, как мне представляется, следует рассказать подробнее. Темы в Quarkly базируются на CSS-переменных. Ключевой особенностью является возможность переиспользования переменных из тем как в пропсах, так и в самой теме, без необходимости использования дополнительных абстракций в виде template-функций и последующей дополнительной обработки со стороны пользователя.

    Чтобы использовать переменные из темы, достаточно описать свойство в теме и обратиться к этому свойству, используя префикс "--".

    Переменные можно использовать как в JSX:

    import Theme from "@quarkly/theme";
     
    const theme = {
       colors: {
           dark: "#04080C",
       },
    };
    export const MyComp = () => (
       <Theme>
           <Box bgc="--colors-dark" height="100px" width="100px" />
       </Theme>
    );

    (Цвет #04080C доступен через свойство --colors-dark)

    Так и в самой теме:

    import Theme from "@quarkly/theme";
     
    const theme = {
       colors: {
           dark: "#04080C",
       },
       borders: {
           dark: "5px solid --colors-dark",
       },
    };
    export const MyComp = () => (
       <Theme>
           <Box border="--borders-dark" height="100px" width="100px" />
       </Theme>
    );

    (Мы переиспользовали переменную из цветов, подключив её в тему borders)

    Для цветов в JSX-разметке предусмотрен упрощенный синтаксис:

    import Theme from "@quarkly/theme";
     
    const theme = {
       colors: {
           dark: "#04080C",
       },
    };
    export const MyComp = () => (
       <Theme>
           <Box bgc="--dark" height="100px" width="100px" />
       </Theme>
    );

    Для работы с медиа-выражениями в темах предусмотрен breakpoint.
    К любому свойству можно добавить префикс в виде имени ключа breakpoint'а.

    import Theme from "@quarkly/theme";
     
    const theme = {
       breakpoints: {
           sm: [{ type: "max-width", value: 576 }],
           md: [{ type: "max-width", value: 768 }],
           lg: [{ type: "max-width", value: 992 }],
       },
       colors: {
           dark: "#04080C",
       },
       borders: {
           dark: "5px solid --colors-dark",
       },
    };
    export const MyComp = () => (
       <Theme>
           <Box
               md-bgc="--dark"
               border="--borders-dark"
               height="100px"
               width="100px"
           />
       </Theme>
    );
    

    С исходным кодом тем можно ознакомиться здесь.

    Эффекты


    Основным отличием Atomize от Styled-System являются «effects». Что это и зачем это нужно?
    Давайте представим, что вы создаете компонент Button, меняете у него color и border, но как назначить стили на hover, focus etc? Тут на помощь приходят эффекты.

    При создании компонента достаточно передать объект с конфигурацией:

    const MySuperButton = atomize.button({
     effects: {
       hover: ":hover",
       focus: ":focus",
       active: ":active",
       disabled: ":disabled",
     },
    });

    Ключом является префикс в имени пропса, а значением — CSS-селектор. Таким образом мы закрыли потребность во всех псевдо-классах.

    Теперь если мы укажем префикс hover к любому CSS-свойству, то оно будет применено при определенном эффекте. Например, при наведении курсора:

    ReactDOM.render(<MySuperButton hover-bgc="blue" />, root);

    Также эффекты можно сочетать с медиа-выражениями:

    ReactDOM.render(<MySuperButton md-hover-bgc="blue" />, root);

    Несколько примеров


    Чтобы визуализировать информацию выше, давайте теперь соберем какой-нибудь интересный компонент. Мы приготовили два примера:


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

    Но это не всё


    Второе предназначение Atomize, как вы упомянули выше, это создание виджетов в Quarkly на основе пользовательских react-компонентов.

    Для этого достаточно обернуть ваш компонент в Atomize и описать его конфигурацию, чтобы Quarkly смог понять, какие свойства можно интерактивно редактировать:

    export default atomize(PokemonCard)(
     {
       name: "PokeCard",
       effects: {
         hover: ":hover",
       },
       description: {
         // past here description for your component
         en: "PokeCard — my awesome component",
       },
       propInfo: {
         // past here props description for your component
         name: {
           control: "input",
         },
       },
     },
     { name: "pikachu" }
    );

    Поля конфигурации для компонента выглядят так:

    • effects — определяет браузерные псевдоклассы (hover, focus, etc);
    • description — описание компонента, которое будет появляться при наведении курсора на его название;
    • propInfo — конфигурация контролов, которые будут отображаться в правой панели (вкладка props).

    Как определить пропсы, которые будут выводиться на правой панели (вкладка props):

    propInfo: {
       yourCustomProps: { // имя свойства
           description: { en: "test" }, // описание с учетом локализации
           control: "input" // тип контрола
       }
    }

    Возможные варианты контролов:

    • input,
    • select,
    • color,
    • font,
    • shadow,
    • transition,
    • transform,
    • filter,
    • background,
    • checkbox-icon,
    • radio-icon,
    • radio-group,
    • checkbox.

    Ещё один пример. Здесь мы добавили свой компонент в систему и теперь можем редактировать его интерактивно:


    Спасибо тем, кто осилил материал до конца! Заранее извиняюсь за сумбур, это первый опыт написания такого рода статей. Буду благодарен за критику.

    А теперь конкурс!


    Дабы слегка подогреть интерес сообщества к более тесному знакомству с Atomize, мы решили пойти по простому и понятному (как и сам Atomize) пути — мы запускаем конкурс!

    Вся информация о сроках, правилах и призах доступна на официальном сайте конкурса.

    Если коротко: для участия и победы необходимо придумать (или найти готовый) интересный и полезный компонент на React и адаптировать его под требования Atomize. Мы выберем и наградим победителей сразу в нескольких номинациях. Дополнительные призы от нашей команды в случае добавления вашего компонента в Quarkly гарантированы.
    Quarkly
    Делаем процесс создания сайтов и приложений проще

    Комментарии 12

      +1
      Спасибо за статью )

      Ребят, поясните про эффекты?
      > Основным отличием Atomize от Styled-System являются «effects».

      Как вы относитесь к возможным проблемам из-за того, что это в принципе всё же Side Effects?
        +2
        Спасибо за комментарий :)

        Задача абстракции effects (в контексте atomize) — предоставлять возможность атомарно описывать какие-либо составные свойства.

        Конфигурация вида
        {hover: ":hover"}

        позволяет описать стили на состояние наведения
        <Button hover-color=”purple” />

        В результате получится такой CSS:
        .%класс CSS-in-JS библиотеки%:hover { color: purple }

        Возможно указать любой CSS-селектор, :hover лишь частный случай.

        На счет того, является ли сайд эффектом такое поведение — думаю, что нет, т.к сама функция активации CSS является чистой.
        +1

        А чем вам не угодил jss?

          +3
          Спасибо за комментарий)

          Тут все зависит от того, что конкретно вы имеете в виду под jss.

          Если вопрос про то, почему бы просто не использовать styled-components или emotion, то для решения нашей задачи (создание виджетов с возможностью интерактивного редактирования) концепция атомарного CSS подходит больше.
          Виджеты никогда не будут зависеть от замыкания, их можно будет свободно перемещать, как между собой, так и между страницами проекта, да что уж, можно просто скопировать код и переместить в другой проект.

          А если вопрос про то, почему мы используем именно styled-components, а не библиотеку JSS(https://github.com/cssinjs/jss), то тут дело в том, что данный проект плохо поддерживают, я более года назад создавал issue, на него так ответа и не получил.
          +1

          А можно было не придумывать свой велосипед, и взять велосипед, который делают в Яндексе — reshadow. Пока использовал только на одном проекте, всем доволен и рекомендую к использованию. Стили компилируются в статику, для динамических значений — пробрасываются CSS-переменные, позволяет использовать любой препроцессор, писать обычный CSS с вменяемой подсветкой синтаксиса и автокомплитом, а так же полностью отказаться от CSS-классов.

            +1
            Спасибо за комментарий и наводку)
            Про данный проект я раньше не слышал, сейчас ознакомился.

            reshadow выглядит как очень интересный и стоящий проект — он решает одну из основных проблем концепции JSS — производительность.

            Я изучу возможность замены styled-components (atomize — обертка над SC или Emotion) на reshadow, это бы нам очень помогло.

            Но отказаться от концепции атомарного CSS мы не можем.
            Без сомнения, на данный момент трудно найти инструментарий в виде линтеров, токенизаторов для редакторов кода, но все впереди :)
            А плюсов конкретно под нашу задачу более чем достаточно :)
              0

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

            +1
            Спасибо за статью! Как дальше планируете развивать проект и библиотеку?
              +3
              У проектов планов прям море, но есть роад-мэп, который обязательно поделимся.
              Пост про проект и его планы в целом в планах, есть о чем рассказать.

              По библиотеке есть как минимум план оформить, документацию в нормальном виде выложить. Но более внятно Саша уже завтра ответит, думаю.
              +1
              Подскажите, а как работать с media-выражениями?
                +1
                Спасибо за комментарий)

                Медиа-выражения конструируются относительно абстракции брейкпоинт из темы.
                Для работы достаточно написать префикс (имя брейкпоинта) к любому CSS свойству

                Пример:
                <Text md-color="red">Some text</Text>


                В данном примере применяется свойство color на брейкпоинте с именем md.

                Пример работы здесь по ссылке (измените размер окна превью, чтобы увидеть отличие) codesandbox.io/s/atomize-demo-skhjw?file=/src/Example.js
                0
                Эффекты и работа с media query сильно напоминают tailwind css. При должной настройке тема там также настраивается через css variables. Смотрели в его сторону?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое