
Поделюсь несколькими практиками, которые использую при создании React-компонентов. Заинтересованных прошу под кат.
Установка параметров по условию
Возьмем для примера кнопку и его частные состояния — размер и цвет.
Обычно в коде я встречаю что-то типа такого:
import React from 'react'; function Button({ size, skin, children }) { return ( <button className={`button${size ? ` button_size_${size}` : ''}${skin ? ` button_skin_${skin}` : '' }`}> {children} </button> ); }
Её читабельность вроде бы сохранена, но что если у нас будет ещё больше состояний?
Я подумал, что гораздо легче собирать все доступные состояния в коллекцию, где ключом будет название состояния, а значением будет имя класса. Удобный просмотр, удобное использование. К тому же мы будем экономить на операциях со строками.
Итак:
import React from 'react'; import classNames from 'classnames'; const SIZE_CLASSES = { small: 'button_size_small', medium: 'button_size_medium', large: 'button_size_large', }; const SKIN_CLASSES = { accent: 'button_skin_accent', primary: 'button_skin_primary', }; function Button({ size, skin, children }) { return ( <button className={classNames( 'button', SIZE_CLASSES[size], SKIN_CLASSES[skin], )} > {children} </button> ); }
Для удобства присвоения классов я использую утилиту classnames.
Напишем напоследок ещё какой-нибудь пример.
import React from 'react'; const RESULT_IMAGES = { 1: '/img/medal_gold.svg', 2: '/img/medal_silver.svg', 3: '/img/medal_bronze.svg', }; function Result({ position }) { return ( <div> <img src={RESULT_IMAGES[position]} /> <h2>Поздравляем! Вы на {position} месте!</h2> </div> ); }
Установка тега по условию
Иногда возникает потребность выставлять тот или иной HTML-тег или React компонент при рендере в зависимости от условия. Для примера, конечно же, возьмём нашу любимую кнопку, потому что она прекрасно демонстрирует проблему. С точки зрения UI она обычно выглядит как кнопка, но внутри, исходя из ситуации, это может быть либо тег
<button />, либо тег <a />.Если мы не боимся повторения кода, то можно по условию возвращать конкретную обертку с передачей ей всех необходимых параметров или, в конце концов, использовать
React.cloneElement. К примеру:import React from 'react'; function Button({ container, href, type, children }) { let resultContainer = null; if (React.isValidElement(container)) { resultContainer = container; } else if (href) { resultContainer = <a href={href} /> } else { resultContainer = <button type={type} /> } return React.cloneElement( resultContainer, { className: 'button' }, children, ); } Button.defaultProps = { container: null, href: null, type: null, };
Но мне больше импонирует определение переменной через заглавную букву.
import React from 'react'; function Button({ container, href, type, children }) { let Tag = null; if (React.isValidElement(container)) { Tag = container; } else if (href) { Tag = 'a'; } else { Tag = 'button'; } return ( <Tag href={href} type={type} className="button"> {children} </Tag> ); } Button.defaultProps = { container: null, href: null, type: null, };
Смена направления элементов
Возьмём для примера полосу жизни из игр. Слева наш игрок, справа его оппонент. Ограничимся тем, что у каждого будет аватарка и имя. Порядок у нашего игрока аватарка-имя, у оппонента — имя-аватарка. Для определения направления будем использовать параметр
direction.Рассмотрим три способа.
Способ 1. Присваивание в соответствующие переменные по условию.
import React from 'react'; function Player({ avatar, name, direction }) { let pref = null; let posf = null; if (direction === 'ltr') { pref = <img class="player__avatar" src={avatar} alt="Player avatar" />; posf = <span class="player__name">{name}</span>; } else { pref = <span class="player__name">{name}</span>; posf = <img class="player__avatar" src={avatar} alt="Player avatar" />; } return ( <div className="player"> {pref} {posf} </div> ); } Player.defaultProps = { direction: 'ltr', };
Способ 2. Array.prototype.reverse
import React from 'react'; function Player({ avatar, name, direction }) { const arrayOfPlayerItem = [ <img key="avatar" class="player__avatar" src={avatar} alt="Player avatar" />, <span key="name" class="player__name">{name}</span>, ]; if (direction === 'rtl') { arrayOfPlayerItem.reverse(); } return ( <div className="player"> {arrayOfPlayerItem} </div> ); } Player.defaultProps = { direction: 'ltr', };
Способ 3. Манипуляции через CSS.
Все, что нам нужно, это присвоить нужный класс.
import React from 'react'; import classNames from 'classnames'; const DIRECTION_CLASSES = { ltr: 'player_direction_ltr', rtl: 'player_direction_rtl', }; function Player({ avatar, name, direction }) { return ( <div className={classNames( 'player', DIRECTION_CLASSES[direction], )} > <img class="player__avatar" src={avatar} alt="Player avatar" /> <span class="player__name">{name}</span> </div> ); } Player.defaultProps = { direction: 'ltr', };
Далее с помощью CSS есть куча способов решить задачу:
// 1. flexbox .player { display: flex; } .player_direction_rtl .player__avatar { order: 1; } .player_direction_rtl .player__name { order: 0; } // 2. direction .player, .player__avatar, .player__name { display: inline-block; } .player_direction_rtl { direction: rtl; } // 3. float .player { display: inline-block; } .player_direction_rtl .player__avatar, .player_direction_rtl .player__name { float: right; }
Единственным минусом является то, что пользователь, если попытается выделить текст мышкой, получит когнитивный диссонанс, так как фактически мы не меняем расположение элементов в DOM-дереве.
Сохранение ссылки на DOM-элемент
Я использую для этой задачи следующий шаблон.
import React, { Component } from 'react'; class Banner extends Component { componentDidMount() { if (this.DOM.root) { this.DOM.root.addEventListener('transitionend', ...); } } handleRefOfRoot = (node) => { this.DOM.root = node; }; DOM = { root: null, }; render() { return ( <div className="banner" ref={this.handleRefOfRoot}> {this.props.children} </div> ); } }
Вместо того чтобы объявлять функцию прямо в
ref, я выношу её в метод. Благодаря этому при рендере не создается новая функция, а создание стрелочной функции избавляет от привязывания контекста через метод bind. Важно: так стоит делать, только если вы уверены, что ваш компонент будет вызываться несколько раз в короткий промежуток времени, иначе лучше использовать запись ref={(node) => { this.DOM.<node_name> = node; }}, чтобы в лишний раз не загружать память страницы.Сохранение узлов в объект DOM — это аналогия уже устаревшего поля
refs у stateful компонента. Удобно, когда всё хранится в одном месте.Во избежание ошибок, рекомендую проверять наличие DOM-узла перед его использованием, чтобы быть уверенным в получении ссылки на него, то есть что он не
null.Напоследок
Вот ещё несколько статей (старых и не очень) из серии «Best Practices»:
- «Паттерны React», RUVDS.com
- «Шаблоны проектирования в React», RUVDS.com
- «Our Best Practices for Writing React Components», Scott Domes
- «React.js pure render performance anti-pattern», Esa-Matti Suuronen
Также буду рад, если и вы поделитесь в комментариях своими наработанными практиками.
