Pull to refresh

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

Reading time5 min
Views33K

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

Only registered users can participate in poll. Log in, please.
А что вы чаще всего используете?
45.82% classnames115
21.51% clsx54
2.39% Своя имплементация (поделитесь ею в комментариях)6
6.77% Использую Template Literals или Join массива17
1.99% Использую data-атрибуты5
21.51% Мимо крокодил54
251 users voted. 24 users abstained.
Tags:
Hubs:
+3
Comments25

Articles