Как стать автором
Обновить

Учимся правильно писать CSS классы в JSX

Время на прочтение5 мин
Количество просмотров39K

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

Где проблема актуальна? В экосистеме React, и где мы пользуемся замечательным синтаксисом под названием JSX.

Согласно данным NPM Trends, если мы сложим количество использований двух популярных библиотек clsx и classnames для помощи в написании классов, мы увидим, что на данный момент около 300 тысяч проектов не имеют этих библиотек в качестве зависимостей. Добавив сюда 1 миллион 100 тысяч проектов на библиотеке Preact, получим около 1,5 миллиона проектов, где ни одна из этих двух библиотек не используется.

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

react vs classnames vs clsx
react vs classnames vs clsx

Почему именно эти библиотеки?

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

Конечно, 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, то возможно получите такой же ответ как и я:

Просто используй clsx
Просто используй clsx

Из статьи "Вы не знаете библиотеку 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 может оказаться быстрее вашей имплементации.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А что вы чаще всего используете?
43.21% classnames121
22.5% clsx63
2.5% Своя имплементация (поделитесь ею в комментариях)7
6.79% Использую Template Literals или Join массива19
1.79% Использую data-атрибуты5
23.21% Мимо крокодил65
Проголосовали 280 пользователей. Воздержались 29 пользователей.
Теги:
Хабы:
Всего голосов 5: ↑4 и ↓1+3
Комментарии25

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань