Паттерн 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-приложения станут по настоящему гибкими.


Кросс-пост

Комментарии (44)


  1. Dimensi
    17.05.2019 09:48
    +2

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


    1. questor
      17.05.2019 10:12

      Есть ли у вас какая-то готовая альтернатива HOC/миксинам? Что бы предложили вы?


      1. k12th
        17.05.2019 11:15
        +1

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


        1. strannik_k
          17.05.2019 17:10

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

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


          1. Spunreal Автор
            17.05.2019 18:31

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


          1. Kuorell
            17.05.2019 19:56

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


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


            1. k12th
              17.05.2019 20:08

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


              1. strannik_k
                18.05.2019 19:55

                Кстати, я когда-то давно написал для себя плагин с этим приемом из юнити. Но редко была нужда что-то выносить в отдельные сущности, поэтому забросил.
                Вот как это выглядело:
                объявление в самом компоненте.

                class MyComponent1 extends ComponentWithBehaviors {
                  behaviors: [
                  new Behavior1(behavior1Props),
                  new Behavior2(behavior2Props),
                ]}

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

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


                1. k12th
                  18.05.2019 20:44

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


                  1. strannik_k
                    19.05.2019 11:49
                    +1

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


    1. Spunreal Автор
      17.05.2019 18:18

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


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


      1. strannik_k
        18.05.2019 19:33

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


        1. Spunreal Автор
          18.05.2019 20:51

          Директивы в основном используются для выполнения низкоуровневых операций с DOM. А ещё они так же, как и миксины, регистрируются при определении компонента, т.е. подмешиваются в оригинальный компонент. Можно сделать обёртку, которая будет содержать директивы, но это как раз и будет HOC или аналог.


    1. JeStasG
      18.05.2019 20:52

      Именно, теряется вся прелесть 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 (как механизм добавления функциональности в цепочке наследования от родителя к потомкам).


      1. Spunreal Автор
        18.05.2019 21:01

        Extends не аналогичен HOC. В extends вы не сможете частично переопределить шаблон (например, сделать обёртку или изменить содержание слота). Вы либо оставляете шаблон такой, какой был у оригинального компонента, либо пишете новый.


        1. JeStasG
          19.05.2019 20:09
          +1

          Согласен, в случае с шаблонами, с категорией «аналогично» погорячился. Но считаю метод extends более близким к философии Vue с его простотой и ясностью. Что касается непосредственно шаблонов, то для изменения шаблона в расширяющем компоненте, в расширяемом компоненте (компонент заготовка) можно задать шаблон в виде переменной (строка), и в расширяющем компоненте уже переопределись шаблон на основе этой строки, но это уже будет не так явно. А посему, HOC в этом случае (с шаблонами), действительно должен выручить.
          Благодарю, ибо дискуссия позволяет глубже вникнуть в суть вопроса! И спасибо за статью, знания никогда не бывают лишними.


          1. Spunreal Автор
            19.05.2019 20:22

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


            1. JeStasG
              19.05.2019 20:44

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


        1. Tantacula
          20.05.2019 04:50

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


          1. Spunreal Автор
            20.05.2019 07:58

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

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


            1. Tantacula
              20.05.2019 13:43

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


    1. achidutsu
      19.05.2019 21:47

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


  1. andreyiq
    17.05.2019 12:43

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


    1. oxidmod
      17.05.2019 12:54
      +1

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

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

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


      1. andreyiq
        17.05.2019 13:14

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


        1. yarkov
          17.05.2019 14:03

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


          1. andreyiq
            17.05.2019 14:52

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


            1. yarkov
              17.05.2019 14:58

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


              1. andreyiq
                17.05.2019 15:08

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


        1. amelekhin
          17.05.2019 14:07
          +1

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

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


          1. andreyiq
            17.05.2019 14:56

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


            1. Spunreal Автор
              17.05.2019 18:21

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


        1. inzeppelin
          17.05.2019 23:48

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


          1. andreyiq
            18.05.2019 07:42

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


            1. inzeppelin
              19.05.2019 23:01

              Нужен один HOC, который добавляет слой логики и сколько угодно компонентов, которые в него оборачиваются или не оборачиваются. Профит в том, что мы один раз написали v-on:click=«playSound» в HOC'е, а не продублировали эту логику в каждом из «звучащих» компонентов. Я не говорю, что это единственно верный сценарий, но в некоторых случаях такой паттерн может быть очень полезен.


      1. Zenitchik
        17.05.2019 15:08

        Почему попап подтверждения действия должен быть в кнопке, а не в действии? Мне кажется, создать действие confirm и поставить его первым в цепочку действий — логичнее.


        1. Zenitchik
          17.05.2019 17:17

          UPD: то же касается звука и логирования.


          1. psFitz
            18.05.2019 10:48

            +


  1. Zenitchik
    17.05.2019 13:38

    Фундаментальная проблема: этот подход предлагает копаться в шаблоне. А шаблон бы лучше лишний раз не трогать.


    1. Spunreal Автор
      17.05.2019 18:26

      Про какой шаблон идёт речь и почему это лучше не трогать?


  1. kuftachev
    19.05.2019 00:27

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


    1. Spunreal Автор
      19.05.2019 07:55

      А какие альтернативы HOC'а, которые решают подобные задачи? Миксины в данном случае не подходят, extends тоже. У них своя задача, у HOC своя.


      1. inzeppelin
        19.05.2019 23:03

        Справедливости ради нужно отметить, что в Реакте HOC'и постепенно переезжают в Hook'и :)


        1. Spunreal Автор
          20.05.2019 00:05

          Справедливости ради Evan You экспериментировал с хуками уже (естественно, после релиза от реакта). Даже ядро Vue не пришлось трогать :)
          github.com/yyx990803/vue-hooks

          Вообще хуки прикольные, но они не заменят HOC на 100%. У них много общих задач, которые они решают, но есть и различия.