Смогут ли React-хуки заменить компоненты высшего порядка (HOC)?

Перевод статьи Do React Hooks Replace Higher Order Components (HOCs)? с сайта medium.com для css-live.ru, автор — Эрик Эллиотт

«Мандаринка» — снимок Малкольма Карлоу (CC-BY-2.0)

Как только API React-хуков вышел, стало появляться много вопросов о том, сможет ли он заменить другие общие библиотеки и паттерны в экосистеме React+Redux.
Хуки задумывались как замена классам и еще одна прекрасная альтернатива для композиции поведения в отдельные компоненты. Компоненты высшего порядка также полезны для композиции поведения. Очевидно, что их задачи где-то пересекаются, так не заменить ли нам компоненты высшего порядка хуками? Более чем ясно, что некоторые HOC-и они заменить могут. Но нужно ли заменять все ваши HOC-и на React-хуки?

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

Что такое компоненты высшего порядка?

Компонент высшего порядка — компонент, принимающий компонент и возвращающий компонент. HOC-и можно компоновать с помощью бесточечной, декларативной композиции функций. Вот пример, где логируется каждый показ страницы через API /logger:

import React, { useEffect } from 'react';
withPageLogging = Component => props => {
  useEffect(() => {
    fetch(`/logger?location=${ window.location}`);
  }, []);
  return ;
};
export default withPageLogging;

Для его использования можно подмешать его в HOC, которым оборачивается каждая страница:

import compose from 'ramda';
import withRedux from './with-redux.js';
import withAuth from './with-auth.js';
import withLogging from './with-logging.js';
import withLayout from './with-layout.js';
const page = compose(
  withRedux,
  withAuth,
  withLogging,
  withLayout('default'),
);
export default page;

Это создаёт иерархию компонентов, которую можно представить как:

<withRedux>
  <withAuth>
    <withLogging>
      <withLayout>
        <MyPageComponent />
      </withLayout>
    </withLogging>
  </withAuth>
</withRedux>

Чтобы использовать это для страницы:

import page from '../hocs/page.js';
import MyPageComponent from './my-page-component.js';
export default page(MyPageComponent);

Это отличный паттерн, если:

  • Этому HOC не нужно создавать более одного пропса для передачи в дочерний компонент. Желательно, чтобы они вообще не создавали пропсов.
  • Этот HOC не создаёт неявные зависимости, на которые полагаются другие HOC-и или компоненты.
  • У всех (или многих) компонентов в вашем приложении должно быть одно и то же общее поведение.

Замечание: это не строгие правила, от которых вообще нельзя отходить. Это всего лишь подсказки и ориентиры, которые обычно хорошо помогают. Я часто делаю небольшое исключение из правила «Никаких неявных зависимостей» для HOC, который предоставляет мой Redux-провайдер. Его я называю withRedux. Как только Redux подключен, другие HOC-и могут обращаться к состоянию, чтобы авторизовывать пользователей, и так далее.

Паттерн композиции HOC-ов для функциональности, используемой всеми страницами — по-прежнему лучший известный мне подход для множества сквозных задач, таких как общие компоненты, логирование, аутентификация/авторизация, и всего прочего, что используется в нескольких местах, но не требует особенной логики для каждого отдельного компонента.

Зачем использовать HOC-и?

Если полностью отбросить HOC-и, с другими вариантами (напр. хуками и рендер-пропсами) придется делать композицию для каждого случая заново, что потребует массового дублирования кода и множества сиюминутных реализаций одной и той же логики, разбросанных по всему приложению и добавленных в совсем не относящиеся к ним компоненты. Это нарушает ряд фундаментальных принципов разработки программного обеспечения, в том числе:

Поскольку при неправильном применении от HOC-ов могут быть проблемы, не забывайте вот о чем, работая с ними:

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

Замечание: HOC-и — компонуемые функциональные компоненты, способные подмешивать что угодно в пропсы, переданные в обёрнутый компонент, что делает их формой функционального миксина, когда они подмешивают в себя пропсы. Предостережения насчёт функциональных миксинов актуальны и для тех HOC-ов, в которые подмешиваются пропсы.

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

Вот пример реального компонента, использующего хуки:

import React, { useState } from 'react';
import t from 'prop-types';
import TextField, { Input } from '@material/react-text-field';
const noop = () => {};
const Holder = ({
  itemPrice = 175,
  name = '',
  email = '',
  id = '',
  removeHolder = noop,
  showRemoveButton = false,
}) => {
  const [nameInput, setName] = useState(name);
  const [emailInput, setEmail] = useState(email);
  const setter = set => e => {
    const { target } = e;
    const { value } = target;
    set(value);
  };
  return (
    <div className="row">
      <div className="holder">
        <div className="holder-name">
          <TextField label="Name">
            <Input value={nameInput} onChange={setter(setName)} required />
          </TextField>
        </div>
        <div className="holder-email">
          <TextField label="Email">
            <Input
              value={emailInput}
              onChange={setter(setEmail)}
              type="email"
              required
            />
          </TextField>
        </div>
        {showRemoveButton && (
          <button
            className="remove-holder"
            aria-label="Remove membership"
            onClick={e => {
              e.preventDefault();
              removeHolder(id);
            }}
          >
            ×
          </button>
        )}
      </div>
      <div className="line-item-price">${itemPrice}</div>
      <style jsx>{cssHere}</style>
    </div>
  );
};
Holder.propTypes = {
  name: t.string,
  email: t.string,
  itemPrice: t.number,
  id: t.string,
  removeHolder: t.func,
  showRemoveButton: t.bool,
};
export default Holder;

В этом коде используется useState, чтобы отслеживать временное состояние полей формы для имени и почты:

const [nameInput, setName] = useState(name);
const [emailInput, setEmail] = useState(email);

Это состояние используется только для этого компонента, поэтому хуки хорошо подходят для этой задачи.

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

Чем отказываться от всех HOC-ов вообще, лучше быть в курсе того, какие задачи хорошо решаются HOC-ами, а какие нет.

Вот в каких случаях HOC-и не очень уместны:

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

Задачи подходят для HOC-ов, если:

  • Это поведение нужно не для какого-то одного компонента, а для многих (а то и всех) компонентов в приложении
  • Это поведение не требует кучи пропсов, использующего это поведение
  • Компоненты могут использоваться и сами по себе, без этого поведения их HOC-а.
  • Не нужно добавлять свою логику к компоненту, который обернут HOC-ом.

Для всего, что часто используется повсюду во всем приложении, c HOC-ами вы получите простую декларативную, бесточечную реализацию, сосредоточенную в одном месте, тогда как хуки дают множество императивных реализаций для конкретных случаев, что может существенно усложнить весь код интерфейса в вашем приложении и раздуть его объем.

Начните бесплатный урок на EricElliottJS.com

Эрик Эллиотт — автор книг «Композиция программного обеспечения» и «Программирование JavaScript-приложений». Как сооснователь EricElliottJS.com и DevAnywhere.io он обучает разработчиков необходимым навыкам разработки программного обеспечения. А также формирует и консультирует команды разработчиков для крипто-проектов, и он участвовал в разработке программ для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, и лучших артистов, включая Usher, Frank Ocean, Metallica, и многих других.

Он ведет удаленный образ жизни с самой красивой женщиной в мире.

P.S. Это тоже может быть интересно:

3

Комментарии

  1. Алексей

    У вас блоки для кода не очень читабельные — «зебра» отвлекает от текста, да и высота строки очень большая.

  2. Аника

    Есть ли печатная версия книги «Инлайновый контекст форматирования»?

    1. SelenIT

      SelenIT

      Нет, пока только электронная. Вообще тот материал имеет смысл обновить – прошло очень много времени (по меркам веба), браузеры исправили многие соответствующие баги (а некоторые, как Opera Presto, и вовсе вымерли), да и в самом стандарте появилось немало уточнений и пояснений по когда-то спорным моментам. Но, к сожалению, авторам сайта пока с трудом удается выкраивать время на обычные статьи… Возможно, пока хорошим дополнением по теме будет эта переводная статья: https://css-live.ru/css/metriki-shrifta-line-height-vertical-align.html

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Ваш E-mail не будет опубликован

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.