Прокачиваем разработку на Vue с помощью паттернов: HOC

Паттерн HOC (Higher Order Component) очень популярен у React-разработчиков. А вот Vue-разработчики его как-то обходят стороной. Очень зря. Попробуем разобраться в этом.


Что такое HOC?


Компонент высшего порядка (HOC)  —  это функция, которая принимает существующий компонент и возвращает другой компонент, который оборачивает первоначальный, добавляя новую логику.


image


HOC vs mixins


Возможно, многие зададутся вопросом, зачем использовать HOC, когда есть примеси? Они так же добавляют новый функционал компонентам. Что может HOC чего не умеют примеси?


Сперва вспомним, что такое примеси во Vue (определение взято из документации Vue):


Примеси (mixins)  —  это гибкий инструмент повторного использования кода в компонентах Vue. Объект примеси может содержать любые опции компонентов. При использовании компонентом примеси, все опции примеси «подмешиваются» к собственным опциям компонента.

Вроде бы назначение примесей и HOC одинаковое  —  они позволяют расширять функционал разных компонентов. Причём снаружи (использование итогового компонента) это может выглядеть даже одинаково.


Но различие кроется в самом принципе работы HOC и примесей. Примеси “подмешиваются” при объявлении компонента  —  любой экземпляр компонента будет содержать их.
С помощью HOC мы оборачиваем экземпляр компонента не меняя сам компонент, а создавая новый, в том месте, где это требуется. Это значит, что мы влияем только на тот кусок кода, где мы его используем. Благодаря этому мы уменьшаем связанность кода, делаем его более читабельным и гибким.


HOC чем-то напоминает шаблон проектирования decorator.


Создание HOC


Ну что ж. Давайте разберём всё это на примере.


Итак, у нас есть компонент кнопка:


image


Через какое-то время нам вдруг понадобилось логировать нажатие некоторых кнопок (но не всех). Мы можем это сделать через примеси, подмешав код для логирования в компонент кнопки, а потом в нужном месте включать или отключать логирование через какое-нибудь свойство компонента. Но согласитесь, это не очень удобно? А если такого функционала очень много? Одна ошибка  —  и все кнопки могут перестать работать корректно.


HOC в данном случае будет отличным решением. Мы просто обернём в некоторых местах кнопку соответствующим HOC.


Пришло время познать HOC на практике.


Шаг 1. Создаём HOC фукнкцию


Мы помним, что HOC  —  это функция, которая принимает на вход компонент и возвращает другой. Так создадим же такую функцию. Назовём её withLoggerButton.
Именование HOC-функций принято начинать с with  — это своего рода опознавательный знак HOC’ов.


image


Получилась функция, которая принимает на вход компонент Button, а потом возвращает новый компонент. В render-функции мы используем изначальный компонент, но с одним изменением — добавляем событие на клик по DOM-узлу, вывод в консоль надписи clicked.


Если вы не поняли, что тут происходит, что такое h и context, то сперва прочитайте документацию vue по работе render-функций.


В текущем примере я использовал именно функциональный компонент, т.к. мне не требуется состояние. Никто вам не запрещает возвращать обычный компонент вместо функционального, но не забывайте, что функциональные компоненты намного быстрее обычных.


Шаг 2. Используем HOC


Теперь с помощью полученной функции просто создадим новый компонент.


image


Осталось дело за малым — подключить полученный компонент там, где нам нужно логирование нажатий.


Итоговый пример:



Композиция


Всё это конечно здорово, но что делать, если нужна кнопка, которая не только логируется, но и выполняет ещё какое-то действие?


Всё просто. Мы оборачиваемости один HOC в другой. Мы можем так смешать сколько угодно HOC-ов.


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




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


Кросс-пост

Поделиться публикацией

Комментарии 48

    +5
    Не вижу в паттерне HOC ничего хорошего, да, он позволяет композировать интерфейс и все дела, вот только он такой же неявный как и миксины. Если ты не разраб который написал HOC, то тебе придется сидеть и распаковывать компоненты на хоках как конфеты и внимательно рассматривать каждую обертку, чтоб понять как это работает, пытаться понять кто именно прокидывает этот пропс в твой основной компонент.
      0
      Есть ли у вас какая-то готовая альтернатива HOC/миксинам? Что бы предложили вы?
        +2

        Неплохой альтернативой были бы объекты/компоненты в понимании Unity 3D — это достаточно удобная система.
        Миксины к этому довольно близки, кстати, за исключением того что у них нет своих хуков жизненного цикла (их надо вызывать вручную).

          0
          Поддерживаю.
          Проблема уже давно решена в геймдеве. Жаль, что разработчики react и vue туда не заглядывали и свои велосипеды понаписали.

          Spunreal
          Про wrapper hell при использование HOC уже не раз писали сами разработчики реакта.
            0

            Всегда будут плохие кейсы использования паттернов. Любой паттерн нужно применять не ради применения, а где это нужно. Иначе любой паттерн может привести к паттерн-hell.

              0

              Заодно зову k12th
              Спорный момент вытягивания gamedev паттернов в том, что там часто разменивают DX на производиьельность. С точки зрения дизайна api хуков в реакте очень удачное решение. Код группируется и выносится исходя из решаемых задач, а не вокруг методов жизненного цикла(и не приходится иметь десятки таких методов как юнити), при этом все вызовы явные и легко делятся данными. (причем обменном данными тоже явно управляет подключающий компонент, а не конвенции)


              Из минусов сответственно производительность и запрет вызова хука в условных конструкциях. (что впрочем вызвало у нас проблемы лишь однажды за несколько месяцев использования и сразу решилось разделением компонента на 2)

                0

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

                  +1
                  Кстати, я когда-то давно написал для себя плагин с этим приемом из юнити. Но редко была нужда что-то выносить в отдельные сущности, поэтому забросил.
                  Вот как это выглядело:
                  объявление в самом компоненте.
                  class MyComponent1 extends ComponentWithBehaviors {
                    behaviors: [
                    new Behavior1(behavior1Props),
                    new Behavior2(behavior2Props),
                  ]}

                  либо вариант с передачей в компонент через родителя.
                  <MyComponent2 components={[
                    new Behavior1(behavior1Props),
                    new Behavior2(behavior2Props),
                  ]} />

                  Когда нужно выносить много логики, возможно такой подход был бы хорош. Но в простых случаях, когда достаточно 1-2 миксин/HOC, немного многовато кода придеться писать.
                    0

                    Интересно. А это был плагин для какого фреймворка?

                      +1
                      для реакта. Но плагин не допилен был, ну и там в базовом классе ComponentWithBehaviors довольно костыльно происходила подписка на методы жизненного цикла реакт компонента.
                      Выложена одна из сохранившихся промежуточных версий — github.com/sergeysibara/react-behaviours
          0

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


          Так же с помощью HOC можно изменить логику работы библиотечных компонентов не сломав оригинал.

            0
            Я с Vue мало знаком, но вот интересно — в Vue пользовательские директивы не могут быть использованы в качестве альтернативы миксинам? Они вроде ведь тоже могут изменить логику работы, не тронув оригинал.
              +1
              Директивы в основном используются для выполнения низкоуровневых операций с DOM. А ещё они так же, как и миксины, регистрируются при определении компонента, т.е. подмешиваются в оригинальный компонент. Можно сделать обёртку, которая будет содержать директивы, но это как раз и будет HOC или аналог.
            0
            Именно, теряется вся прелесть Vue, с его систематизацией.

            На мой взгляд, ниже приведенная конструкция наследования, аналогично HOC, более наглядна и удобоварима:
            //SecondComponent
            import FirstComponent from './FirstComponent'
            export default { extends: FirstComponent }
            

            Ссылка на документацию: ru.vuejs.org/v2/api/#extends
            Расписаный пример: vuejsdevelopers.com/2017/06/11/vue-js-extending-components

            Если рассматривать все механизмы повторного использования кода во Vue, а не только mixin, то необходимо упомянуть плагины (как механизмы добавления функциональность на глобальном уровне) и provide / inject (как механизм добавления функциональности в цепочке наследования от родителя к потомкам).
              0
              Extends не аналогичен HOC. В extends вы не сможете частично переопределить шаблон (например, сделать обёртку или изменить содержание слота). Вы либо оставляете шаблон такой, какой был у оригинального компонента, либо пишете новый.
                +1
                Согласен, в случае с шаблонами, с категорией «аналогично» погорячился. Но считаю метод extends более близким к философии Vue с его простотой и ясностью. Что касается непосредственно шаблонов, то для изменения шаблона в расширяющем компоненте, в расширяемом компоненте (компонент заготовка) можно задать шаблон в виде переменной (строка), и в расширяющем компоненте уже переопределись шаблон на основе этой строки, но это уже будет не так явно. А посему, HOC в этом случае (с шаблонами), действительно должен выручить.
                Благодарю, ибо дискуссия позволяет глубже вникнуть в суть вопроса! И спасибо за статью, знания никогда не бывают лишними.
                  0

                  HOC — это лишь паттерн. У каждого паттерна есть своя зона применения. Если он не к месту, то он только ухудшит положение. Миксины и extends всё так же полезны, просто в некоторых моментах они проигрывают HOC, а в некоторых выигрывают.

                    0
                    Главное, чтобы патерны не были самоцелью. В смысле, любой ценой использовать их не включая голову, потому что это «модно и круто». Ко всему должен быть критический и рациональный подход.)
                  0

                  Шаблон можно вынести в отдельный файл, написанный на pug или другом шаблонизаторе, поддерживающем наследование/импорт и затем унаследовать его

                    0
                    Я видел этот подход автора статьи. Но у него есть ряд критических недостатков:
                    1) А если я не использую pug или другой шаблонизатор?
                    2) Появляется неявная зависимость в коде.

                    Решение выглядит как большой костыль в попытках расширить непредусмотренную функциональность extend.
                      0

                      Решение не более костыльно, чем использование css-препроцессоров, vuex и Vue router в проекте. Да и почему неявная зависимость, в блоке шаблона шаблонизатор явно указывается.

                0
                Просто всё, что нужно сделать — документировать интефейс написанного HOC. Это исключит споры об архитектуре, которая и так тут всем понятна. Документация того, что сделано, не принята в современном WEB-сообществе от слова 'совсем', это пытаются оправдать тем, что всё opensource и ты всегда можешь сам посмотреть код в качестве документации. Это нереально прокачивает в плане развития конечно, но отнимает значительное время от собственно разработки. Тебя как бы принуждают держать в голове сразу несколько проектов.
                –1

                Три раза прочитал, так и не понял зачем все это. Нужна кнопка с логированием, сделайте компонент назовите LoggerButton и используйте его, там где нужно логирование. Нужно куча всякого функционала, создайте кнопку MyButton, добавьте необходимые св-ва и в соответствии с ними меняйте логику поведения.
                Или я не правильно понял поставленной задачи

                  +3
                  Есть просто кнопка.
                  Нужно сделать:
                  — кнопку с логирование
                  — кнопку со звуком нажатия
                  — кнопку с popup-ом для подтверждения действия

                  А теперь финт, нужн так же кнопки
                  — с логированием и popup-ом
                  — с popup-ом и звуком
                  — с логированием и звуком
                  — со звуком, popup-ом и логированием

                  Правда думаете, что отдельне компоненты лучше делать?
                    –1

                    Один компонент MyButton у него свойство settings: {
                    logger: boolean,
                    sound: boolean,
                    popup: boolean
                    }
                    И обрабатывайте как вам нужно

                      0
                      И будет километровый компонент, ибо логики сочетаний пропсов в нем будет столько, что устанешь писать.
                        –1

                        Чем это хуже кучи непонятных обёрток?

                          +1
                          Кому непонятных? Если человек не понимает что это за обертки и что они делают, то портянку кода он тоже вряд ли разберет.
                          Ну и ниже уже amelekhin написал про принцип единой ответственности. Не должен компонент Button свистеть, пердеть и танцевать вприсядку.
                          Нужная свистящая кнопка? Оборачивай хоком или делай компонент WhistlingButton.
                            0

                            Мне кажется мы друг друга не правильно поняли, вы предложили, то что я написал в первом комете

                        +1
                        Кнопка не должна заниматься логами, и для того, чтобы добавить логи, не следует расширять класс кнопки.

                        См. Принцип единственной ответственности.
                          0

                          Это понятно, здесь видимо логирование просто как пример привели, наверное не самый удачный

                            0
                            В статье пример удачный. За логирование отвечает HOC, а не кнопка.
                          0

                          А если нужен другой компонент со звуком? Ну там панелька какая-нибудь. В этом случае можно переиспользовать HOC и не выдумывать новые компоненты со своими settings. Суть именно в этом. HOC представляет собой независимый слой логики.

                            –1

                            "если нужен другой компонент со звуком?"
                            "не выдумывать новые компоненты"
                            Так нужен компонент или не нужен?

                              0
                              Нужен один HOC, который добавляет слой логики и сколько угодно компонентов, которые в него оборачиваются или не оборачиваются. Профит в том, что мы один раз написали v-on:click=«playSound» в HOC'е, а не продублировали эту логику в каждом из «звучащих» компонентов. Я не говорю, что это единственно верный сценарий, но в некоторых случаях такой паттерн может быть очень полезен.
                          0
                          Почему попап подтверждения действия должен быть в кнопке, а не в действии? Мне кажется, создать действие confirm и поставить его первым в цепочку действий — логичнее.
                            0
                            UPD: то же касается звука и логирования.
                      0
                      Фундаментальная проблема: этот подход предлагает копаться в шаблоне. А шаблон бы лучше лишний раз не трогать.
                        0
                        Про какой шаблон идёт речь и почему это лучше не трогать?
                        +2

                        HOC — это костыль из React, зачем его тащить туда, где даже нет таких архитектурных косяков, ради которых его придумали?

                          0
                          А какие альтернативы HOC'а, которые решают подобные задачи? Миксины в данном случае не подходят, extends тоже. У них своя задача, у HOC своя.
                            0
                            Справедливости ради нужно отметить, что в Реакте HOC'и постепенно переезжают в Hook'и :)
                              0
                              Справедливости ради Evan You экспериментировал с хуками уже (естественно, после релиза от реакта). Даже ядро Vue не пришлось трогать :)
                              github.com/yyx990803/vue-hooks

                              Вообще хуки прикольные, но они не заменят HOC на 100%. У них много общих задач, которые они решают, но есть и различия.
                          +1
                          У HOC 2 серьезные проблемы.
                          1) Если на компонент нужно несколько оберток, то, как написали в первом комменте, приходиться долго разбираться, что там происходит, разматывая эти обертки.
                          2) Нет разделения props.
                          По хорошему, каждая обертка должна получать только props, с которыми она работает. А компонент, только свои props. В HOC в принципе можно сделать так, но он не требует этого и в нем нет готового механизма для этого. Поэтому в больших проектах с активным использованием HOC получается жесть.

                          Имхо, Material UI для react и react-admin (от Marmelab) — хорошие примеры того, к чему HOC могут привести.
                          React devtools показывает с десяток HOC на каждом компоненте. Исходники тоже как-то страшновато выглядят.

                          В общем, я бы не рекомендовал использовать HOC.
                          Ден итак своими Redux и HOC реакт подпортил, давайте хоть в Vue не будем тянуть его наработки) Пусть другим путем идет.
                            0
                            В HOC в принципе можно сделать так, но он не требует этого и в нем нет готового механизма для этого. Поэтому в больших проектах с активным использованием HOC получается жесть.


                            Это как трейты в PHP. Но кто заставляет писать трейт, который опирается на поля класса, его использующего? Не пишите HOC, который зависит от оборачиваемого елемента и они будут простыми и реюзабельными.
                              0
                              Кто научит весь мир так делать? Нам в основном приходится работать с библотеками, где так не сделано. Когда приходишь работать на чужой проект, там тоже так не будет сделано.

                              Ну и в любом случае делают так, что на на вход получаем один набор props, а передается дальше измененный набор props. (Например, в HOC для валидации как минимум добавится prop «прошла ли валидация»).
                              0
                              HOC — это паттерн. Как и с любым другим паттерном, если использовать его бездумно, то он будет только мешать.

                              Возьмём к примеру паттерн декоратор (на примере любого классического серверного языка, пусть даже PHP). Его концепция позволяет сделать то же самое — обернуть какой-нибудь объект в 10 обёрток, тем самым вызвать wrapped-hell. Но почему-то им до сих пор пользуются и пользуются успешно. Всё потому, что используют его там, где он принесёт много пользы, а не везде.
                              Нет идеального паттерна, есть хорошие паттерны для решения конкретных проблем.

                              В HOC в принципе можно сделать так, но он не требует этого и в нем нет готового механизма для этого.

                              HOC — это частный случай HOF (Higher Order Function). Вся концепция HOC — это получить один (на самом деле можно и несколько, но это особо не важно) компонент параметром и на основе полученных данных выплюнуть другой компонент. В целом вообще без разницы, что за код там будет внутри. Хотите как-то делить props? Пожалуйста, никто не запрещает. Хотите добавить какие-то сложные условия работы? Пожалуйста, никто не запрещает. Хотите с помощью HOC реализовать паттерн Container Component? Опять же, никто не запрещает.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое