Поделюсь несколькими практиками, которые использую при создании 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
Также буду рад, если и вы поделитесь в комментариях своими наработанными практиками.
Комментарии (4)
Alternator
01.06.2018 21:55Последний пункт неактуален с появлением React.createRef()
inomdzhon92 Автор
01.06.2018 22:24Я не нашёл удобства в этом относительно новом API. Мне бы хотелось не писать
current
.
Если говорить про хранение элементов в св-веDOM
, то придётся писатьthis.DOM.root.current
.
hazratgs
Я думаю это вполне очевидные вещи и назвать их «cheatsheet» не знаю даже, с некоторыми даже не соглашусь (первый пример), удобнее скорее использовать styled-components или style-name
inomdzhon92 Автор
Согласен, для нас с вами — очевидны. Но являются ли они таковыми для всех? К примеру, для тех, кто только начинает свой путь в веб-разработку?
На счёт styled-components и т.п. Для первого пункта я привёл пример, что подход применим не только для подстановке того или иного класса.