Поделюсь несколькими практиками, которые использую при создании 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»:

  1. «Паттерны React», RUVDS.com
  2. «Шаблоны проектирования в React», RUVDS.com
  3. «Our Best Practices for Writing React Components», Scott Domes
  4. «React.js pure render performance anti-pattern», Esa-Matti Suuronen

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

Комментарии (4)


  1. hazratgs
    01.06.2018 11:17

    Я думаю это вполне очевидные вещи и назвать их «cheatsheet» не знаю даже, с некоторыми даже не соглашусь (первый пример), удобнее скорее использовать styled-components или style-name


    1. inomdzhon92 Автор
      01.06.2018 21:29

      Согласен, для нас с вами — очевидны. Но являются ли они таковыми для всех? К примеру, для тех, кто только начинает свой путь в веб-разработку?
      На счёт styled-components и т.п. Для первого пункта я привёл пример, что подход применим не только для подстановке того или иного класса.


  1. Alternator
    01.06.2018 21:55

    Последний пункт неактуален с появлением React.createRef()


    1. inomdzhon92 Автор
      01.06.2018 22:24

      Я не нашёл удобства в этом относительно новом API. Мне бы хотелось не писать current.
      Если говорить про хранение элементов в св-ве DOM, то придётся писать this.DOM.root.current.