Паттерн HOC (Higher Order Component) очень популярен у React-разработчиков. А вот Vue-разработчики его как-то обходят стороной. Очень зря. Попробуем разобраться в этом.
Что такое HOC?
Компонент высшего порядка (HOC)? —? это функция, которая принимает существующий компонент и возвращает другой компонент, который оборачивает первоначальный, добавляя новую логику.
HOC vs mixins
Возможно, многие зададутся вопросом, зачем использовать HOC, когда есть примеси? Они так же добавляют новый функционал компонентам. Что может HOC чего не умеют примеси?
Сперва вспомним, что такое примеси во Vue (определение взято из документации Vue):
Примеси (mixins)? —? это гибкий инструмент повторного использования кода в компонентах Vue. Объект примеси может содержать любые опции компонентов. При использовании компонентом примеси, все опции примеси «подмешиваются» к собственным опциям компонента.
Вроде бы назначение примесей и HOC одинаковое? —? они позволяют расширять функционал разных компонентов. Причём снаружи (использование итогового компонента) это может выглядеть даже одинаково.
Но различие кроется в самом принципе работы HOC и примесей. Примеси “подмешиваются” при объявлении компонента? —? любой экземпляр компонента будет содержать их.
С помощью HOC мы оборачиваем экземпляр компонента не меняя сам компонент, а создавая новый, в том месте, где это требуется. Это значит, что мы влияем только на тот кусок кода, где мы его используем. Благодаря этому мы уменьшаем связанность кода, делаем его более читабельным и гибким.
HOC чем-то напоминает шаблон проектирования decorator.
Создание HOC
Ну что ж. Давайте разберём всё это на примере.
Итак, у нас есть компонент кнопка:
Через какое-то время нам вдруг понадобилось логировать нажатие некоторых кнопок (но не всех). Мы можем это сделать через примеси, подмешав код для логирования в компонент кнопки, а потом в нужном месте включать или отключать логирование через какое-нибудь свойство компонента. Но согласитесь, это не очень удобно? А если такого функционала очень много? Одна ошибка ?— ?и все кнопки могут перестать работать корректно.
HOC в данном случае будет отличным решением. Мы просто обернём в некоторых местах кнопку соответствующим HOC.
Пришло время познать HOC на практике.
Шаг 1. Создаём HOC фукнкцию
Мы помним, что HOC ?— ?это функция, которая принимает на вход компонент и возвращает другой. Так создадим же такую функцию. Назовём её withLoggerButton.
Именование HOC-функций принято начинать с with ?— это своего рода опознавательный знак HOC’ов.
Получилась функция, которая принимает на вход компонент Button, а потом возвращает новый компонент. В render-функции мы используем изначальный компонент, но с одним изменением?—?добавляем событие на клик по DOM-узлу, вывод в консоль надписи clicked.
Если вы не поняли, что тут происходит, что такое h и context, то сперва прочитайте документацию vue по работе render-функций.
В текущем примере я использовал именно функциональный компонент, т.к. мне не требуется состояние. Никто вам не запрещает возвращать обычный компонент вместо функционального, но не забывайте, что функциональные компоненты намного быстрее обычных.
Шаг 2. Используем HOC
Теперь с помощью полученной функции просто создадим новый компонент.
Осталось дело за малым?—?подключить полученный компонент там, где нам нужно логирование нажатий.
Итоговый пример:
Композиция
Всё это конечно здорово, но что делать, если нужна кнопка, которая не только логируется, но и выполняет ещё какое-то действие?
Всё просто. Мы оборачиваемости один HOC в другой. Мы можем так смешать сколько угодно HOC-ов.
Так же для композиции есть очень много готовых функций и библиотек, которые облегчают композицию.
HOC? — ?это простой, но в то же время очень мощный паттерн. Он используется в основе множества библиотек. Он не является серебряной пулей или полной заменой миксинов и механизма наследования компонентов. Применяйте его с умом в комбинации с другими паттернами и ваши Vue-приложения станут по настоящему гибкими.
Комментарии (44)
andreyiq
17.05.2019 12:43Три раза прочитал, так и не понял зачем все это. Нужна кнопка с логированием, сделайте компонент назовите LoggerButton и используйте его, там где нужно логирование. Нужно куча всякого функционала, создайте кнопку MyButton, добавьте необходимые св-ва и в соответствии с ними меняйте логику поведения.
Или я не правильно понял поставленной задачиoxidmod
17.05.2019 12:54+1Есть просто кнопка.
Нужно сделать:
— кнопку с логирование
— кнопку со звуком нажатия
— кнопку с popup-ом для подтверждения действия
А теперь финт, нужн так же кнопки
— с логированием и popup-ом
— с popup-ом и звуком
— с логированием и звуком
— со звуком, popup-ом и логированием
Правда думаете, что отдельне компоненты лучше делать?andreyiq
17.05.2019 13:14Один компонент MyButton у него свойство settings: {
logger: boolean,
sound: boolean,
popup: boolean
}
И обрабатывайте как вам нужноyarkov
17.05.2019 14:03И будет километровый компонент, ибо логики сочетаний пропсов в нем будет столько, что устанешь писать.
andreyiq
17.05.2019 14:52Чем это хуже кучи непонятных обёрток?
yarkov
17.05.2019 14:58Кому непонятных? Если человек не понимает что это за обертки и что они делают, то портянку кода он тоже вряд ли разберет.
Ну и ниже уже amelekhin написал про принцип единой ответственности. Не должен компонентButton
свистеть, пердеть и танцевать вприсядку.
Нужная свистящая кнопка? Оборачивай хоком или делай компонентWhistlingButton
.andreyiq
17.05.2019 15:08Мне кажется мы друг друга не правильно поняли, вы предложили, то что я написал в первом комете
amelekhin
17.05.2019 14:07+1Кнопка не должна заниматься логами, и для того, чтобы добавить логи, не следует расширять класс кнопки.
См. Принцип единственной ответственности.
inzeppelin
17.05.2019 23:48А если нужен другой компонент со звуком? Ну там панелька какая-нибудь. В этом случае можно переиспользовать HOC и не выдумывать новые компоненты со своими settings. Суть именно в этом. HOC представляет собой независимый слой логики.
andreyiq
18.05.2019 07:42"если нужен другой компонент со звуком?"
"не выдумывать новые компоненты"
Так нужен компонент или не нужен?inzeppelin
19.05.2019 23:01Нужен один HOC, который добавляет слой логики и сколько угодно компонентов, которые в него оборачиваются или не оборачиваются. Профит в том, что мы один раз написали v-on:click=«playSound» в HOC'е, а не продублировали эту логику в каждом из «звучащих» компонентов. Я не говорю, что это единственно верный сценарий, но в некоторых случаях такой паттерн может быть очень полезен.
kuftachev
19.05.2019 00:27HOC — это костыль из React, зачем его тащить туда, где даже нет таких архитектурных косяков, ради которых его придумали?
Spunreal Автор
19.05.2019 07:55А какие альтернативы HOC'а, которые решают подобные задачи? Миксины в данном случае не подходят, extends тоже. У них своя задача, у HOC своя.
inzeppelin
19.05.2019 23:03Справедливости ради нужно отметить, что в Реакте HOC'и постепенно переезжают в Hook'и :)
Spunreal Автор
20.05.2019 00:05Справедливости ради Evan You экспериментировал с хуками уже (естественно, после релиза от реакта). Даже ядро Vue не пришлось трогать :)
github.com/yyx990803/vue-hooks
Вообще хуки прикольные, но они не заменят HOC на 100%. У них много общих задач, которые они решают, но есть и различия.
Dimensi
Не вижу в паттерне HOC ничего хорошего, да, он позволяет композировать интерфейс и все дела, вот только он такой же неявный как и миксины. Если ты не разраб который написал HOC, то тебе придется сидеть и распаковывать компоненты на хоках как конфеты и внимательно рассматривать каждую обертку, чтоб понять как это работает, пытаться понять кто именно прокидывает этот пропс в твой основной компонент.
questor
Есть ли у вас какая-то готовая альтернатива HOC/миксинам? Что бы предложили вы?
k12th
Неплохой альтернативой были бы объекты/компоненты в понимании Unity 3D — это достаточно удобная система.
Миксины к этому довольно близки, кстати, за исключением того что у них нет своих хуков жизненного цикла (их надо вызывать вручную).
strannik_k
Поддерживаю.
Проблема уже давно решена в геймдеве. Жаль, что разработчики react и vue туда не заглядывали и свои велосипеды понаписали.
Spunreal
Про wrapper hell при использование HOC уже не раз писали сами разработчики реакта.
Spunreal Автор
Всегда будут плохие кейсы использования паттернов. Любой паттерн нужно применять не ради применения, а где это нужно. Иначе любой паттерн может привести к паттерн-hell.
Kuorell
Заодно зову k12th
Спорный момент вытягивания gamedev паттернов в том, что там часто разменивают DX на производиьельность. С точки зрения дизайна api хуков в реакте очень удачное решение. Код группируется и выносится исходя из решаемых задач, а не вокруг методов жизненного цикла(и не приходится иметь десятки таких методов как юнити), при этом все вызовы явные и легко делятся данными. (причем обменном данными тоже явно управляет подключающий компонент, а не конвенции)
Из минусов сответственно производительность и запрет вызова хука в условных конструкциях. (что впрочем вызвало у нас проблемы лишь однажды за несколько месяцев использования и сразу решилось разделением компонента на 2)
k12th
Ну понятно, что если перенести этот прием из юнити прям один-в-один, то, вероятно, получится не очень — все-таки задачи разные и в геймдеве сложность куда выше. Но насчет DX, кажется, дело субъективное — мне лично в юнити приятнее работать, чем с реактом.
strannik_k
Кстати, я когда-то давно написал для себя плагин с этим приемом из юнити. Но редко была нужда что-то выносить в отдельные сущности, поэтому забросил.
Вот как это выглядело:
объявление в самом компоненте.
либо вариант с передачей в компонент через родителя.
Когда нужно выносить много логики, возможно такой подход был бы хорош. Но в простых случаях, когда достаточно 1-2 миксин/HOC, немного многовато кода придеться писать.
k12th
Интересно. А это был плагин для какого фреймворка?
strannik_k
для реакта. Но плагин не допилен был, ну и там в базовом классе ComponentWithBehaviors довольно костыльно происходила подписка на методы жизненного цикла реакт компонента.
Выложена одна из сохранившихся промежуточных версий — github.com/sergeysibara/react-behaviours
Spunreal Автор
Миксины модифицируют базовый компонент. HOC же возращает новый, а оригинальный так и останется нетронутым. (с) Расширяй, но не изменяй.
Так же с помощью HOC можно изменить логику работы библиотечных компонентов не сломав оригинал.
strannik_k
Я с Vue мало знаком, но вот интересно — в Vue пользовательские директивы не могут быть использованы в качестве альтернативы миксинам? Они вроде ведь тоже могут изменить логику работы, не тронув оригинал.
Spunreal Автор
Директивы в основном используются для выполнения низкоуровневых операций с DOM. А ещё они так же, как и миксины, регистрируются при определении компонента, т.е. подмешиваются в оригинальный компонент. Можно сделать обёртку, которая будет содержать директивы, но это как раз и будет HOC или аналог.
JeStasG
Именно, теряется вся прелесть Vue, с его систематизацией.
На мой взгляд, ниже приведенная конструкция наследования, аналогично HOC, более наглядна и удобоварима:
Ссылка на документацию: ru.vuejs.org/v2/api/#extends
Расписаный пример: vuejsdevelopers.com/2017/06/11/vue-js-extending-components
Если рассматривать все механизмы повторного использования кода во Vue, а не только mixin, то необходимо упомянуть плагины (как механизмы добавления функциональность на глобальном уровне) и provide / inject (как механизм добавления функциональности в цепочке наследования от родителя к потомкам).
Spunreal Автор
Extends не аналогичен HOC. В extends вы не сможете частично переопределить шаблон (например, сделать обёртку или изменить содержание слота). Вы либо оставляете шаблон такой, какой был у оригинального компонента, либо пишете новый.
JeStasG
Согласен, в случае с шаблонами, с категорией «аналогично» погорячился. Но считаю метод extends более близким к философии Vue с его простотой и ясностью. Что касается непосредственно шаблонов, то для изменения шаблона в расширяющем компоненте, в расширяемом компоненте (компонент заготовка) можно задать шаблон в виде переменной (строка), и в расширяющем компоненте уже переопределись шаблон на основе этой строки, но это уже будет не так явно. А посему, HOC в этом случае (с шаблонами), действительно должен выручить.
Благодарю, ибо дискуссия позволяет глубже вникнуть в суть вопроса! И спасибо за статью, знания никогда не бывают лишними.
Spunreal Автор
HOC — это лишь паттерн. У каждого паттерна есть своя зона применения. Если он не к месту, то он только ухудшит положение. Миксины и extends всё так же полезны, просто в некоторых моментах они проигрывают HOC, а в некоторых выигрывают.
JeStasG
Главное, чтобы патерны не были самоцелью. В смысле, любой ценой использовать их не включая голову, потому что это «модно и круто». Ко всему должен быть критический и рациональный подход.)
Tantacula
Шаблон можно вынести в отдельный файл, написанный на pug или другом шаблонизаторе, поддерживающем наследование/импорт и затем унаследовать его
Spunreal Автор
Я видел этот подход автора статьи. Но у него есть ряд критических недостатков:
1) А если я не использую pug или другой шаблонизатор?
2) Появляется неявная зависимость в коде.
Решение выглядит как большой костыль в попытках расширить непредусмотренную функциональность extend.
Tantacula
Решение не более костыльно, чем использование css-препроцессоров, vuex и Vue router в проекте. Да и почему неявная зависимость, в блоке шаблона шаблонизатор явно указывается.
achidutsu
Просто всё, что нужно сделать — документировать интефейс написанного HOC. Это исключит споры об архитектуре, которая и так тут всем понятна. Документация того, что сделано, не принята в современном WEB-сообществе от слова 'совсем', это пытаются оправдать тем, что всё opensource и ты всегда можешь сам посмотреть код в качестве документации. Это нереально прокачивает в плане развития конечно, но отнимает значительное время от собственно разработки. Тебя как бы принуждают держать в голове сразу несколько проектов.