Казалось бы такая простая тема как написание css-классов не должна быть проблемой, однако я встречал довольно много проектов, где допускаются ошибки, пишутся непроизводительные велосипеде, что приводит к ошибкам на продакшене и плохо читаемому коду.
Где проблема актуальна? В экосистеме React, и где мы пользуемся замечательным синтаксисом под названием JSX.
Согласно данным NPM Trends, если мы сложим количество использований двух популярных библиотек clsx
и classnames
для помощи в написании классов, мы увидим, что на данный момент около 300 тысяч проектов не имеют этих библиотек в качестве зависимостей. Добавив сюда 1 миллион 100 тысяч проектов на библиотеке Preact, получим около 1,5 миллиона проектов, где ни одна из этих двух библиотек не используется.
Также бывает, что библиотека есть, просто не используется разработчиками.
Почему именно эти библиотеки?
Они добавляют удобство за счет продуманного синтаксиса, который прекрасно подходит для решения большинства задач, связанных с множеством классов, которые мы задаем согласно определенным условиям.
Конечно, Javascript предлагает множество вариантов решения такой проблемы. Например, Template Literals или использование массива с последующим join
.
Но давайте для начала рассмотрим примеры кода.
Вот, простой компонент, который должен иметь условный класс в зависимости от определенных условий:
const Button = (props) => (
<button className={`btn ${props.pressed && 'btn-pressed'}`}>
{props.children}
</button>
)
Терпимо? В целом да, если не смотреть на феерию скобочек и кавычек в конце '}`}>
Однако данный код зарендерит следующее в зависимости от значения pressed
:
<!--- pressed: true --->
<button class="btn btn-pressed">Button</button>
<!--- pressed: false --->
<button class="btn false">Button</button>
<!--- pressed: undefined --->
<button class="btn undefined">Button</button>
Окей. Нехорошо. Давайте попробуем тернарный оператор:
const Button = (props) => (
<button className={`btn ${props.pressed ? 'btn-pressed' : ''}`}>
{props.children}
</button>
)
и... получаем лишний пробел в конце:
<button class="btn btn-pressed">Button</button>
<button class="btn ">Button</button>
Это уже не так критично, однако, давайте рассмотрим пример из реального мира, путем усложнения количества свойств, а также добавим возможность передавать класс с помощью props:
const Card = (props) => {
const { className, elevated, outlined } = props;
return (
<div className={`card ${className ? className : ''} ${outlined ? 'card-outlined' : ''} ${elevated ? 'card-elevated' : ''}}>
{props.children}
</div>
)
}
Конечно, можно использовать промежуточные переменные, использовать массив и метод join(' ')
, иначе у нас появятся двойные или тройные пробелы. Или добавить очередной webpack-плагин, который бы это исправил... или... просто использовать библиотеку:
import clsx from 'clsx'
const Card = (props) => {
const { className, elevated, outlined } = props;
return (
<div className={clsx('card', className, {
'card-outlined': outlined,
'card-elevated': elevated,
})}>
{props.children}
</div>
)
}
В данном случае эта библиотека добавит вам 228 дополнительных байт, что будет ничем не хуже самописного велосипеда. А благодаря популярности и унифицированного синтакса, который работает в этих обоих библиотеках, это больший выигрыш в отношении поддержки такого кода.
Как результат мы получили более читаемый код, который легче поддерживать, и который создает аккуратный html без случайных символов и слов.
Внимательный читатель заметит, что использовать глобальные css-классы нынче моветон, и будет прав. Используя css-modules или JSS подход намного повышает надежность продуциемого кода, искореняя потенциальные конфликты стилей.
import clsx from 'clsx'
import classes from './index.modules.css'
const Card = (props) => {
const { className, elevated, outlined } = props;
return (
<div className={clsx(classes.root, className, {
// получается все так же чисто и аккуратно
[classes.outlined]: outlined,
[classes.elevated]: elevated,
})}>
{props.children}
</div>
)
}
Так какую библиотеку использовать? clsx или classnames
Если вы введете этот вопрос в Google, то возможно получите такой же ответ как и я:
Из статьи "Вы не знаете библиотеку classnames" Арека Нао вы сможете узнать, что библиотека classnames
имеет более богатый функционал, которым... никто не пользуется. А синтаксис библиотеки clsx
такой же, при том, что она быстрее и легче (правильно: функционала-то меньше).
Причина в высокой скорости библиотеки -- ее простота и использование for
, while
циклов, конкатенция строк вместо операций над массивами. Исходный код на GitHub.
Позвольте, но есть же альтернатива
Конечно же есть. Один из паттернов, про который все забыли -- это так называемые data-
атрибуты. Ничто не мешает заменить лапшу из css-классов btn btn-elevated btn-large
на data-variant="elevated"
data-size="large"
.
А затем, написать подобный css:
.button {}
.button[data-size="small"] {}
.button[data-size="large"] {}
.button[data-variant="elevated"] {}
.button:disabled,
.button[data-state="disabled"] {
/** Последний вариант иногда нужен,
чтобы иметь возможность кликнуть по кнопке
для получения определенного фидбека
*/
}
К сожалению, у этого подхода на самом деле один жирный минус. И нет, это не производительность браузера при поиске селекторов. Так никто не делает. А это значит отсутствие привычных инструментов: минификация css-классов доступна из коробки, а здесь придется что-то придумывать. Неудобный синтаксис, если мы используем JSS решения с object нотацией.
Напишите в комментариях, что вы думаете по поводу такого подхода?
Бонус для разработчиков на Preact
Одной из киллер-фич этой библиотеки на заре была возможность использования ключевого слова class
для использования его в JSX. Я помню, как способ задания css с помощью className
был камнем преткновения для множества разработчиков, которым показали React и JSX. Однако... время показало, что className
удобнее своей универсальностью. И сейчас я покажу почему:
В примере сверху мы разбирали вариант, где в компонент передавался параметр className
, который обычно добавляется к корневому DOM-элементу компонента.
И если мы еще можем передавать class
внутри JSX разметки, использовать этот ключ при декомпозиции объекта props
или указывать его в интерфейсах Typescript уже не получится никак. Как результат, на моей практике я сталкивался с таким зоопарком в наименовании: customClass
, parentClass
, rootClass
, mainClass
, и так далее. Как результат, вместо упрощения мы получили усложнение и неконсистентность.
Поэтому во всех Preact проектах я использую привычное всем className
вместе с набором совместимости preact/compat
.
Бонус к бонусу или ремарка о статическом кодоанализе
Если что-то можно автоматизировать ценой пары кликов, оно должно быть автоматизировано.
Для того, чтобы запретить эти нестандартные атрибуты в JSX можно сконфигурировать очень популярный плагин для eslint следующим образом:
"react/forbid-component-props": ["on", {
"forbid": ["class", "customClass", "parentClass"]
}]
Мораль сей басни такова
Лишняя пара-тройка килобайт всегда стоит того, чтобы ваш код был более читаемым, поддерживаемым и содержал меньше ошибок. А порой, такая библиотека как clsx
может оказаться быстрее вашей имплементации.