Стилизация. Довольно насущный для меня момент. Несмотря на годы работы с 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)
Alexandroppolus
12.03.2022 22:09Цсс-модули и стайлед-компоненты хорошо стыкуются с пользовательскими компонентами, у которых есть подпорка className (бывают, правда, ещё пропсы вроде anotherClassName, с которыми стайледы почти не дружат).
Как с этим обстоит дело у сабжа?
iminside Автор
13.03.2022 09:28Тут всё хорошо :)
<Div root red className="anotherClassName" />
на выходе даст, что-то типа
<div class="_root_78bp722 _red_53ds782 anotherClassName"></div>
т.е. значение свойства `className`, если оно определено, будет проброшено до целевого компонента
Alexandroppolus
13.03.2022 13:53Я немного о другом. Вот допустим календарь - кастомный компонент. У него есть пропсы className и tileClassName. Как их задать через Candy?
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"
Alexandroppolus
14.03.2022 06:02Ок, понятно. То есть по сути возвращаемся всё к тем же цсс-модулям.
Разный подход к встроенным и кастомным компонентам - не очень хорошо (хотя могу ошибаться).
kahi4
Очень интересный подход! А как передать не булево свойство внутрь?
И более менее сложный лэйаут будет требовать десятка дивов, импорты превратятся в Div as ComponentWrapper.
Я бы посмотрел в сторону чего-то такого:
Вроде в рамках нормального css (sass), но гибче.
iminside Автор
Внутрь ничего передавать не нужно, расценивайте это не как улучшение styled-components, а больше как улучшение css-modules, т.е. мы избавились от использования библиотек типа `classnames` и ручной генерации значения свойства `className`, а всё остальное осталось как и было, т.е.
В том то и дело, что всё будет гораздо проще :)
при этом в стилях у нас
Синтетический пример, но думаю замысел понятен :)
кстати спасибо за идею! обязательно покопаю в эту сторону, дабы упростить подобное
iminside Автор
изначально не правильно понял ваш замысел, подумал что вы хотите пробрасывать свойства внутрь, как в styled-components, поэтому исправляюсь и прошу первый абзац моего ответа изложить в следующей редакции:
для динамического определения какого-то свойства компонента необходимо использовать подход css-modules, а именно передачу через свойство `style`
спасибо)
iminside Автор
развивая эту тему - можно заюзать установку значений переменных через атрибут style, т.е. например (тут нужно соглашение) все свойства, начинающиеся с двойного нижнего подчеркивания
конвертировать в переменные, получая на выходе
доступ к которым получать через `var(...)`
iminside Автор
что я собственно и сделал