Смогут ли 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-и, с другими вариантами (напр. хуками и рендер-пропсами) придется делать композицию для каждого случая заново, что потребует массового дублирования кода и множества сиюминутных реализаций одной и той же логики, разбросанных по всему приложению и добавленных в совсем не относящиеся к ним компоненты. Это нарушает ряд фундаментальных принципов разработки программного обеспечения, в том числе:
- Отсутствие повторения (DRY)
- Делай что-то одно (DOT) (из философии Unix)
- Разделение ответственности
- Принцип наименьшего знания (закон Деметры)
Поскольку при неправильном применении от 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. Это тоже может быть интересно:
None Found
У вас блоки для кода не очень читабельные — «зебра» отвлекает от текста, да и высота строки очень большая.
+1, спустя несколько минут чтения появилось жгучее желание читать по диагонали, просто устали глаза. Особенно мучительно читать сериф с италиком на тетрадной подложке(
Есть ли печатная версия книги «Инлайновый контекст форматирования»?
Нет, пока только электронная. Вообще тот материал имеет смысл обновить – прошло очень много времени (по меркам веба), браузеры исправили многие соответствующие баги (а некоторые, как Opera Presto, и вовсе вымерли), да и в самом стандарте появилось немало уточнений и пояснений по когда-то спорным моментам. Но, к сожалению, авторам сайта пока с трудом удается выкраивать время на обычные статьи… Возможно, пока хорошим дополнением по теме будет эта переводная статья: https://css-live.ru/css/metriki-shrifta-line-height-vertical-align.html