Компоненты высшего порядка¶
Компонент высшего порядка (Higher-Order Component, HOC) — это один из продвинутых способов для повторного использования логики. HOC не являются частью API React, но часто применяются из-за композиционной природы компонентов.
Говоря просто, компонент высшего порядка — это функция, которая принимает компонент и возвращает новый компонент.
1 2 3 |
|
Если обычный компонент преобразует пропсы в UI, то компонент высшего порядка преобразует компонент в другой компонент.
HOC часто встречаются в сторонних библиотеках, например connect
в Redux и createFragmentContainer
в Relay.
В этой главе мы обсудим чем полезны компоненты высшего порядка и как их создавать.
HOC для сквозной функциональности¶
Примечание
В прошлом мы рекомендовали примеси для реализации сквозной функциональности, но со временем выяснилось, что от них больше вреда, чем пользы. Узнайте, почему мы решили убрать примеси и как переписать старые компоненты.
Традиционные компоненты подразумевают многократное использование, но не позволяют с лёгкостью решить некоторые проблемы.
Рассмотрим пример CommentList
, который получает список комментариев из внешнего источника данных и отображает их:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Теперь мы решили реализовать новый компонент, который отслеживает изменения конкретной публикации и повторяет уже знакомый нам шаблон:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Разница между CommentList
и BlogPost
в том, что они вызывают разные методы DataSource
и рендерят разный вывод. Однако в большинстве своём они похожи:
- Оба компонента подписываются на оповещения от
DataSource
при монтировании. - Оба меняют внутреннее состояние при изменении
DataSource
. - Оба отписываются от
DataSource
при размонтировании.
Можете представить, что в больших приложениях связка «подписаться на DataSource
, затем вызвать setState
» повторяется очень часто. Было бы здорово абстрагировать эту функциональность и использовать её в других компонентах.
Давайте реализуем функцию withSubscription
— она будет создавать компоненты и подписывать их на обновления DataSource
(наподобие CommentList
и BlogPost
). Функция будет принимать оборачиваемый компонент и через пропсы передавать ему новые данные:
1 2 3 4 5 6 7 8 9 |
|
Первый параметр — это оборачиваемый компонент. Второй — функция, которая извлекает нужные нам данные, она получает DataSource
и текущие пропсы.
Когда CommentListWithSubscription
и BlogPostWithSubscription
рендерятся, они передают в CommentList
и BlogPost
обновлённые данные DataSource
через проп data
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Заметьте, что HOC ничего не меняет и не наследует поведение оборачиваемого компонента, вместо этого HOC оборачивает оригинальный компонент в контейнер посредством композиции. HOC является чистой функцией без побочных эффектов.
Вот и всё! Оборачиваемый компонент получает все пропсы, переданные контейнеру, а также проп data
. Для HOC не важно, как будут использоваться данные, а оборачиваемому компоненту не важно, откуда они берутся.
Так как withSubscription
— это обычная функция, то мы можем убрать или добавить любое количество аргументов. Например, мы могли бы сделать конфигурируемым название пропа data
и ещё больше изолировать HOC от оборачиваемого компонента. Также мы можем добавить аргумент для конфигурации shouldComponentUpdate
или источника данных. Всё это возможно, потому что HOC полностью контролирует процесс создания компонента.
Взаимодействие между withSubscription
и оборачиваемым компонентом осуществляется с помощью пропсов, так же, как и между обычными компонентами. Благодаря этому мы можем с лёгкостью заменить один HOC на другой, при условии, что они передают одни и те же пропсы в оборачиваемый компонент. Это может пригодиться если, например, мы решим поменять библиотеку получения данных.
Не мутируйте оборачиваемый компонент. Используйте композицию.¶
Не поддавайтесь соблазну менять прототип компонента (или мутировать его любым другим способом) внутри HOC.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
В приведённом выше примере мы не можем повторно использовать InputComponent
отдельно от EnhancedComponent
. Важнее то, что если мы захотим обернуть EnhancedComponent
в другой HOC, который тоже меняет componentWillReceiveProps
, то мы сотрём функциональность заданную первым HOC! Более того, EnhancedComponent
не работает с функциональными компонентами, потому что у них отсутствуют методы жизненного цикла.
Мутирующие HOC являются хрупкой абстракцией, они конфликтуют с другими HOC, мы не сможем просто применять их без того, чтобы знать что именно они меняют.
Вместо мутации, компоненты высшего порядка должны применять композицию, оборачивая компонент в контейнер:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Этот HOC обладает такой же функциональностью, как и предыдущий, но не создаёт конфликтов с другими HOC и работает как с функциональными, так и с классовыми компонентами. Более того, HOC реализован с помощью чистой функции, поэтому его можно совмещать с другими HOC, или даже самого с собой.
Возможно, вы уже заметили сходство между HOC и компонентами-контейнерами. Напомним, что при помощи контейнеров мы обычно разделяем общую функциональность от частной. Например, в контейнере мы будем управлять внутренним состоянием или подпиской на внешние ресурсы, и через пропсы передавать данные в компоненты, ответственные за рендер UI. При реализации HOC мы тоже используем контейнеры. Можно сказать что HOC — это инструмент для параметризированного создания контейнеров.
Соглашение: передавайте посторонние пропсы оборачиваемому компоненту¶
HOC добавляют компонентам функциональность, но они не должны менять их оригинальное предназначение. Ожидается, что интерфейс компонента, который вы возвращаете из HOC, будет похож на интерфейс оборачиваемого компонента.
Пропсы, которые напрямую не связаны с функциональностью HOC, должны передаваться без изменений оборачиваемому компоненту. Рендер-метод большинства HOC похож на следующий:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Такое соглашение помогает создавать гибкие повторно используемые компоненты.
Соглашение: Максимизируем композитивность¶
Не все HOC выглядят одинаково. Некоторые принимают всего лишь один аргумент — оборачиваемый компонент:
1 |
|
Обычно HOC принимают несколько аргументов. В данном примере из Relay, мы используем объект конфигурации с описанием данных для компонента:
1 2 3 4 |
|
Самый распространённый способ вызова HOC выглядит так:
1 2 3 4 5 |
|
Удивлены? Давайте разберём эту строку по частям.
1 2 3 4 5 6 7 8 |
|
Другими словами, connect
— это функция высшего порядка, которая возвращает компонент высшего порядка!
Такая форма может показаться запутанной и ненужной, но есть и преимущества. Вызов connect
возвращает HOC с подписью Component => Component
. Функции с одинаковым типом результата и единственного аргумента легко совмещаются в композиции.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
(Поэтому мы можем использовать connect
и другие расширяющие функциональность HOC в качестве экспериментальных JavaScript декораторов.)
Вы можете найти вспомогательную функцию compose
во многих сторонних библиотеках, включая lodash (под названием lodash.flowRight
), Redux, и Ramda.
Соглашение: добавьте отображаемое имя для лёгкой отладки¶
Созданные HOC компоненты-контейнеры отображаются в консоли инструментов разработки React наряду с другими компонентами. Для более лёгкой отладки вы можете задать имя, которое подскажет, что определённый компонент был создан с помощью HOC.
Самый распространённый способ — это обернуть имя оборачиваемого компонента. Например, если вы назвали компонент высшего порядка withSubscription
, а имя оборачиваемого компонента было CommentList
, то отображаемое имя будет WithSubscription(CommentList)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Предостережения¶
Вы можете столкнуться с неочевидными проблемами, когда работаете с компонентами высшего порядка.
Не используйте HOC внутри рендер-метода¶
Алгоритм сравнения React (известный как согласование или reconciliation) использует тождественность компонентов чтобы определить нужно ли обновить существующее поддерево, или убрать и монтировать вместо него новое. Если компонент, полученный из render
, идентичен (===
) компоненту из предыдущего рендера, то React рекурсивно продолжит сравнивать поддерево. Если компоненты не равны, React полностью удалит и заменит старое поддерево.
Обычно нас это не беспокоит. Однако, важно учитывать что мы не можем применять компоненты высшего порядка внутри рендер-метода компонента:
1 2 3 4 5 6 7 |
|
Проблема не только в производительности. Повторное монтирование компонента обнуляет его состояние, а также состояние его дочерних компонентов.
Не применяйте HOC в определении другого компонента. Сначала нужно отдельно получить компонент из HOC, и только потом использовать его. Таким образом React будет сравнивать один и тот же компонент при повторном рендере.
При необходимости (в редких случаях) можно динамически применять HOC в методах жизненного цикла или конструкторе компонента.
Копируйте статические методы¶
Иногда бывает полезно определить статические методы компонента. Например, статический метод getFragment
библиотеки Relay позволяет составить композицию из фрагментов данных GraphQL.
Когда мы применяем HOC, то заворачиваем оригинальный компонент в контейнер. Поэтому у нового компонента не будет статических методов оригинального компонента.
1 2 3 4 5 6 7 8 9 |
|
Скопируйте недостающие методы в контейнер:
1 2 3 4 5 6 7 8 |
|
К сожалению, вы должны точно знать какие методы копировать. Вы можете воспользоваться hoist-non-react-statics, чтобы автоматически скопировать не связанные с React статические методы:
1 2 3 4 5 6 7 8 |
|
Другое возможное решение — экспортировать статические методы отдельно от компонента.
1 2 3 4 5 6 7 8 9 |
|
Рефы не передаются¶
По соглашению компоненты высшего порядка передают оборачиваемому компоненту все пропсы, кроме рефов. ref
на самом деле не проп, как, например, key
, и поэтому иначе обрабатывается React. Реф элемента, созданного компонентом из HOC, будет указывать на экземпляр ближайшего в иерархии контейнера, а не на оборачиваемый компонент.
Вы можете решить эту проблему с помощью API-метода React.forwardRef
(добавлен в React 16.3). Узнать подробнее в главе Перенаправление рефов.