Смогут ли 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. Это тоже может быть интересно: