Frontender Magazine

React-компоненты с привкусом БЭМ

Уверен, все фронтенд-разработчики слышали про БЭМ и React.

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

Мы в Wimdu используем правила по наименованию компонентов из БЭМ и некоторые концепты SMACSS.

Например, типичный компонент выглядит так:

<header className='header header--landing'>
    <h1 className='header__title'>…</h1>
    <h2 className='header__subtitle is-hidden'>…</h2>
</header>

Этот компонент состоит из:

Компоненты

Ниже в примерах будем использовать ES6 и функциональные компоненты для описания компонентов React.

Попробуем сделать компонент Header с заголовком и подзаголовком.

Начнём с простого:

// components/header/index.js

export default ({ modifier, title, subtitle }) => (
    <header className=`header header--${modifier}`>
        <h1 className='header__title'>{ title }</h1>
        <h2 className=`header__subtitle ${!subtitle ? 'is-hidden' : ''}`>
            { subtitle }
        </h2>
    </header>
)

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

import Header from 'components/header'

// например, поместим компонент на главную страницу
ReactDOM.render(
    <Header
        modifier='landing'
        title='Городские апартаменты'
        subtitle='Более 5 миллионов бронирований'
    />
, node)

// или на страницу /about
ReactDOM.render(
    <Header
        modifier='about'
        title='Про Wimdu'
        subtitle='Познакомьтесь с нашей командой и посмотрите, что мы умеем'
    />
, node)

Сделаем модификатор необязательным. Чтобы не писать кучу тернарных операторов, возьмем удобную библиотеку для работы с классами classnames.

// components/header/index.js

import cx from 'classnames'

export default ({ modifier, title, subtitle }) => (
    <header className={cx('header', { [`header--${modifier}`]: modifier })}>
        <h1 className='header__title'>{ title }</h1>
        <h2 className={cx('header__subtitle', { 'is-hidden': subtitle })}>
            { subtitle }
        </h2>
    </header>
)

Получили то, что и хотели — компонент, готовый к использованию в разных контекстах. Решение рабочее, хотя и выглядит довольно неаккуратно. Если нам предстоит написать несколько десятков таких компонентов, лучше придумать что-то более элегантное.

Удобство использования

Честно говоря, мы хотим, чтобы Header выглядел максимально просто: блок с модификатором и двумя элементами внутри.

Попробуем вынести элементы Header, Title и Subtitle из компонента наружу:

// components/header/index.js

import { Header, Title, Subtitle } from './elements'

export default ({ modifier, title, subtitle }) => (
    <Header modifier={modifier}>
        <Title>{ title }</Title>
        <Subtitle hidden={!subtitle}>{ subtitle }</Subtitle>
    </Header>
)

То, что нужно! Похоже на слегка параметризованный html-компонент из начала статьи.

Теперь определим вынесенные наружу элементы.

Основная идея — взять React-элементы и поколдовать над их свойствами.

Для изменения свойств будем использовать библиотеку transform-props-with:

// components/header/elements.js

import cx from 'classnames'
import tx from 'transform-props-with'

const addElementStyles = (oldProps) => {
    const { hidden, modifier, name, ...props } = oldProps

    return {
        className: cx({
            [`header__${name}`]: name,
            [`header__${name}--${modifier}`]: name && modifier,
            ['is-hidden']: hidden
        }),
        ...props
    }
}

// добавляем React-элементу header класс .header
export const Header = tx({ className: 'header' })('header')
// добавляем React-элементу h1 класс .header__title
export const Title = tx([{ name: 'title' }, addElementStyles])('h1')
// добавляем React-элементу h2 класс .header__subtitle
export const Subtitle = tx([{ name: 'subtitle' }, addElementStyles])('h2')

Фактически, мы декорировали React-элементы header, h1 и h2.

Теперь при передаче в наши атомарные компоненты свойства modifier или hidden для них будет сгенерирован соответствующий класс.

Встречаем dumb-bem

dumb-bem — это библиотека, которую мы написали для создания атомарных React-компонентов с BEM/SMACSS правилами по наименованию классов.

Передавая имя блока в фyнкцию dumbBem, вы получаете функцию-декоратор, которая меняет свойства у React-элемента.

import dumbBem from 'dumb-bem'
import tx from 'transform-props-with'

// здесь header — название BEM блока
const dumbHeader = dumbBem('header')

const Header = tx(dumbHeader)('header')
const Title = tx([dumbHeader, { element: 'title' }])('h1')
const Subtitle = tx([dumbHeader, { element: 'subtitle' }])('h2')

export default ({ modifier, title, subtitle }) => (
    <Header modifier={modifier}>
        <Title>{ title }</Title>
        <Subtitle hidden={!subtitle}>{ subtitle }</Subtitle>
    </Header>
)

Страницы проекта: github и npm.

Более подробно об использовании данной библиотеки читайте в документации либо в моей следующей статье.

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Александр Гудулин

Комментарии (7 комментариев, если быть точным)

Автар пользователя
yunusga

Наверное уже пора расширить методологию БЭМ до БЭМСостояние

Автар пользователя
web2easy

Почему бы не использовать модификатор состояния header__subtitle--is--hidden заместо примеси из SMACSS?

Автар пользователя
agudulin

Почему бы не использовать модификатор состояния header__subtitle--is--hidden заместо примеси из SMACSS?

@web2easy в нашей команде принят такой стиль, поэтому изначально в библиотеке был реализован подход с SMACSS состояниями.

Мы поняли, что разумно вынести состояния SMACSS из библиотеки и оставить только то, что связано с БЭМ, и уже делаем это: https://github.com/agudulin/dumb-bem/issues/6 ;)

Автар пользователя
olegdizus

Горячо люблю БЭМ, но с приходом React (и Preact) в БЭМе смысла не вижу. Ведь при использовании этих библиотек в помощь так и просятся CSS-модули. Вся верстка разбивается на независимые компоненты, которые почти всегда по сути являются БЭМ-блоками. А CSS-модули дают некое подобие областей видимости классов. Тем более, что именование классов модулей может быть настроено на БЭМ-подобный синтаксис. Кроме того, для React есть библиотека react-css-modules, которая еще более упрощает работу с классами. На Preact она тоже прекрасно стает (при помощи preact-compat). И кстати в Preact-е функциональность classnames работает из коробки (при 3kb gzip!). Что упустил?

Автар пользователя
agudulin

@olegdizus, история всегда банальная: проект большой и долгий, хочется переписать его на каждый новый фреймворк, но бизнес в этом не заинтересован :)

Автар пользователя
SPAHI4

@agudulin, ну, css modules - это не фреймворк. И вполне себе удобный. Уж точно лучше громоздкого BEM

Автар пользователя
agudulin

@SPAHI4, я не про именно CSS-модули говорил, а про подход использования чего-либо нового в большом проекте.

Разумеется, можно потихоньку внедрять: в новых компонентах, к примеру, использовать CSS-модули, настроить систему сборки для части проекта иначе и т.п., потихоньку переписывать старые стили компонентов... Но нужно доказать себе, команде и бизнесу, что у этого есть вэлью. Я его, если честно, не вижу.

Если у вас есть опыт миграции большого проекта с БЭМ на CSS-модули — буду рад услышать! :)