Стилизация. Довольно насущный для меня момент. Несмотря на годы работы с React, стартуя новый проект, я каждый раз задумываюсь о стилизации. Я перепробовал многие её способы, больше и дольше всего я работал с css-modules и styled-components. Сегодня я хочу предложить вам рассмотреть еще один не менее интересный вариант.


Для начала я хочу вкратце пробежаться по вышеупомянутым приёмам, чтобы объяснить, что именно меня в них не устраивает, и почему я придумал еще один тысячапервый стандарт.


CSS-modules


Просто, чётко, интуитивно понятно. Никаких проблем с пересечением стилей.


import styles from './component.css'

function Component() {
    return <div className={styles.root}>...</div>
}

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


import styles from './component.css'

function Component(props) {
    let className = styles.root

    if (props.red) className += ' ' + styles.red

    return <div className={className}>...</div>
}

Да, есть конечно библиотеки типа classnames, призванные решить эту проблему...


import cn from 'classnames'
import styles from './component.css'

function Component(props) {
    const className = cn(styles.root, props.red && styles.red)

    return <div className={className}>...</div>
}

но даже тут постоянный бойлерплейт вида className={cn(...)} начинает порядком раздражать, хочется чего-то более локаничного.


Styled components


Ммм, няшка… Был момент, когда я так сильно обрадовался этой находке, что незамедлительно переписал один большой проект на новый лад.


const Root = styled.div`
    color: white;
`

function Component() {
    return <Root>...</Root>
}

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


Но! Розовые очки рано или поздно испаряются и та же лапша, только уже под новым соусом, снова начинает раздражать.


const Root = styled.div`
    color: white;

    ${(props) =>
        props.red &&
        css`
            background-color: red;
        `}
`

function Component(props) {
    return <Root red={props.red}>...</Root>
}

Добавим сюда постоянную нагрузку на генератор оригинальных названий для компонентов (тот, что в черепной коробке), а также излишнее абстрагирование в виде сокрытия реальных названий html-тегов за выдуманными псевдонимами.


Не один год я использовал этот подход и всё же пришёл к выводу, что во первых — я хочу писать css-код в родных ему css-файлах, во вторых — я хочу видеть в компонентах привычную html-разметку из привычных html-тегов.


Candy


Скажу сразу — это proposal. Чисто по опыту — велосипедостроение часто оборачивается пустой, никому не нужной тратой времени. Поэтому в этот раз я набросал рабочий вариант и, прежде чем пилить в сторону production-ready, решил прощупать почву этой статьёй.


Идея очень проста. Мы пишем привычный css, sass, less, вот прям как по кайфу :)


/* styles.css */

.root {
    color: white;
}

.red {
    backgroung-color: red;
}

А затем импортируем из файла стилей компоненты-html-теги, которые имеют булевы свойства связанные с названиями css-классов.


import { Div } from './styles.css'

function Component(props) {
    return (
        <Div root red={props.red}>
            ...
        </Div>
    )
}

А выполнить такой финт нам позволяет webpack-загрузчик candy-loader.


Вот это поворот :)


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


Импортировать можно любой стандартный html-тег. Названия классов на выходе формируются как в css-modules, т.е. одноименные классы из разных источников не будут пересекаться.


Можно подключать css-файлы, получая доступ к их стилям.


/* styles.css */
@import 'grid.css';

/*...*/

import { Div } from './styles.css'

function Component(props) {
    return (
        <Div root red={props.red} col_xs_12 col_sm_8>
            ...
        </Div>
    )
}

Настройка


candy-loader базируется на postcss, поэтому для дополнительной настройки можно использовать стандартный конфигурационный файл


// .postcssrc.js

module.exports = {
    plugins: {
        autoprefixer: isProduction,
    },
    processOptions: {
        map: isDevelopment,
    },
}

Ну и конечно же Intellisense!


Для этого есть typescript-plugin-candy. Довольно простой в установке и настройке плагин, позволяющий получить автокомплит и проверку типов.



Что скажете?


Жизнеспособная идея? Стоит её дальше развивать и совершенствовать?
Ниже я привел ссылки на исходники и заранее подготовленный стартер-репо на случай, если вам захочется с этим поиграть.



Спасибо за потраченное время, буду рад вашим комментариям и предложениям по развитию.

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


  1. kahi4
    12.03.2022 16:57

    Очень интересный подход! А как передать не булево свойство внутрь?

    И более менее сложный лэйаут будет требовать десятка дивов, импорты превратятся в Div as ComponentWrapper.

    Я бы посмотрел в сторону чего-то такого:

    div#Wrapper { // возможность указать тэг
      color: green;
      
      .dangerous { // булево
         color: red;
      }
      
      background: attr(background); // получаем значение из проперти. к слову, это можно вообще нативно через дата аттрибут сделать, но вроде как там возможности пока что сильно лимитированны 
    }

    Вроде в рамках нормального css (sass), но гибче.


    1. iminside Автор
      12.03.2022 17:31

      А как передать не булево свойство внутрь?

      Внутрь ничего передавать не нужно, расценивайте это не как улучшение styled-components, а больше как улучшение css-modules, т.е. мы избавились от использования библиотек типа `classnames` и ручной генерации значения свойства `className`, а всё остальное осталось как и было, т.е.

      .avatar {
        width: 40px;
        height: 40px;
        background-size: cover;
        background-position: center;
        background-repeat: no-repeat;
        border-radius: 50%;
      }
      import { Div } from './styles.css'
      
      function Avatar(props){
       	return <Div avatar style={{backgroundImage: `url(${props.src})`}} /> 
      }

      И более менее сложный лэйаут будет требовать десятка дивов, импорты превратятся в Div as ComponentWrapper.

      В том то и дело, что всё будет гораздо проще :)

      import { Div } from './styles.css'
      
      function Layout(props){
       	return <Div wrapper>
          	<Div header />
          	<Div aside />
              <Div content>{props.children}</Div>
        		<Div footer />
        </Div>
      }

      при этом в стилях у нас

      .wrapper{}
      .header{}
      .aside{}
      .content{}
      .footer{}

      Синтетический пример, но думаю замысел понятен :)

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

      кстати спасибо за идею! обязательно покопаю в эту сторону, дабы упростить подобное

      style={{backgroundImage: `url(${props.src})`}}


    1. iminside Автор
      13.03.2022 09:38

      А как передать не булево свойство внутрь?

      изначально не правильно понял ваш замысел, подумал что вы хотите пробрасывать свойства внутрь, как в styled-components, поэтому исправляюсь и прошу первый абзац моего ответа изложить в следующей редакции:

      для динамического определения какого-то свойства компонента необходимо использовать подход css-modules, а именно передачу через свойство `style`

      спасибо)


    1. iminside Автор
      13.03.2022 10:14

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

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

      <Div name __fontSize="14px">John</Div>

      конвертировать в переменные, получая на выходе

      <div class="name" style="--fontSize: 14px">John</div>

      доступ к которым получать через `var(...)`

      .name {
      	font-size: var(--fontSize);
      }


    1. iminside Автор
      13.03.2022 11:12

      что я собственно и сделал


  1. Alexandroppolus
    12.03.2022 22:09

    Цсс-модули и стайлед-компоненты хорошо стыкуются с пользовательскими компонентами, у которых есть подпорка className (бывают, правда, ещё пропсы вроде anotherClassName, с которыми стайледы почти не дружат).

    Как с этим обстоит дело у сабжа?


    1. iminside Автор
      13.03.2022 09:28

      Тут всё хорошо :)

      <Div root red className="anotherClassName" />

      на выходе даст, что-то типа

      <div class="_root_78bp722 _red_53ds782 anotherClassName"></div>

      т.е. значение свойства `className`, если оно определено, будет проброшено до целевого компонента


      1. Alexandroppolus
        13.03.2022 13:53

        Я немного о другом. Вот допустим календарь - кастомный компонент. У него есть пропсы className и tileClassName. Как их задать через Candy?


        1. iminside Автор
          13.03.2022 14:24

          аа, вот вы о чем )

          сейчас это можно сделать, например, так: в файле стилей определить

          /* style.css */
          
          :global .calendar_custom_classname {
              color: rebeccapurple;
          }
          
          :global .calendar_custom_tile_classname {
              color: red;
          }

          затем

          import './style.css'
          
          function MyApp() { 
            return ( 
              <Calendar 
              	className="calendar_custom_classname" 
              	tileClassName="calendar_custom_tile_classname" /> 
            )
          }

          можно конечно рассмотреть вопрос о том, чтобы `candy-loader` помимо компонентов-тегов экспортировал ссылки на локальные названия классов, тогда можно будет сочетать подход css-modules, типа

          /* style.css */
          
          .calendar_custom_classname {
              color: rebeccapurple;
          }
          
          .calendar_custom_tile_classname {
              color: red;
          }
          import {calendar_custom_classname, calendar_custom_tile_classname} from './style.css'
          
          function MyApp() { 
            return (
              <Calendar 
              	className={calendar_custom_classname} 
              	tileClassName={calendar_custom_tile_classname} /> 
            )
          }

          тут `calendar_custom_classname` и `calendar_custom_tile_classname` будут содержать что-то типа "_calendar_custom_tile_classname_0ksfd33"


          1. Alexandroppolus
            14.03.2022 06:02

            Ок, понятно. То есть по сути возвращаемся всё к тем же цсс-модулям.

            Разный подход к встроенным и кастомным компонентам - не очень хорошо (хотя могу ошибаться).


  1. PavelMelnik94
    14.03.2022 19:05

    Здорово, мне очень понравилась идея