Как я уже писал в своих предыдущих статьях я работал и с polymer и с vue в связке с redux. Поэтому хотелось бы поделиться опытом, связанным со спецификой использования redux в данных библиотеках. Рассматривать будем на простейших атомарных контролах: нативных (input, checkbox) и обернутых, в виде компонентов данных библиотек.

В статье я опуская описание настройки интеграции redux с polymer и vue, а так же описание азов самого redux, дабы не эту тему хочу раскрыть в статье.

0. Введение


Сначала вспомним один из основных принципов redux:
The only way to change the state is to emit an action, an object describing what happened.

Исходя из него ясно, что напрямую мы не можем изменить состояние, а сделать это можем только через диспатч экшена после наступление необходимого event'а.

Схематично это можно изобразить так:

Как видим наблюдается односторонний поток данных.

1. Нативные контролы


polymer


Очень удобная вещь в polymer при связке с redux дак это односторонний биндинг.

template:

<input value="[[propFromReduxStore]]" on-change="changeText"></input>

js-code:

changeInput: function(e) {
  this.dispatch("setText", e.currentTarget.value);
}

Поэтому с input все, в принципе стандартно: при событии change диспатчим action и после чего измененное значение попадает в propFromReduxStore и контрол перерендерится уже с новым значением.

vue


C vue немного другая ситуация, в нем нет как такагого одностороннего биндинга, как в polymer. Но подобную функциональность можно достигнуть через модификатор sync

UPD: Спасибо mayorovp за наводку: на самом деле никакой .sync не нужен. По поводу биндинга: v-model дает двусторонний биндинг, а как раз :value — односторонний. Поэтому в примере ниже не нужен .sync.
template:

Старый код
<input :value.sync="propFromReduxStore" @change="changeText"></input>


<input :value="propFromReduxStore" @change="changeText"></input>


js-code:

changeInput: function(e) {
  this.actionsRedux("setText", e.currentTarget.value);
}

Остальное все как и в варианте с polymer.

2. Компоненты


С компонентами сложней, так как это совокупность методов, событий, «завернутые» в компоновку html-элементов в виде шаблона.

Схематичное описание работы компонента:

Как видим о событии компонента мы узнаем уже обо всем постфактум, что противоречит принципу redux описанному выше. И что бы избежать неконсистентной ситуации, когда контрол уже перерендерился в виду своего внутреннего состояния, а прибинденная модель еще не поменялась через action и не соответствует данному представлению необходимо производить дополнительные действия для блокировки прямого изменения state.

polymer


На примере компонента paper-checkbox

template:

<paper-checkbox checked="[[propFromReduxStore]]" on-tap="changeCheck"></paper-checkbox>

js-code:

changeCheck: function(e) { //Здесь ловим клик по компоненту
  //Предотвращаем bubbling события, что бы компонент сразу не перерендерил компонент 
  e.stopPropagation();
  this.dispatch("setChecked", !this.propFromReduxStore);
}

vue


На примере компонента el-checkbox

template:

UPD: Код ниже подчеркивает сам подход через нативные ивенты, но для конкретного контрола(имеется в виду его внутренняя архитектура) имеет место три вариант:

  <el-checkbox :value="checked" @change="changeCheck">
  </el-checkbox>
  <el-checkbox :value="checked" @click.prevent.stop.native="changeCheck">
  </el-checkbox>
  <el-checkbox v-model="checked" @click.prevent.stop.native="changeCheck">
  </el-checkbox>

Старый вариант
<el-checkbox v-model="propFromReduxStore" @click.stop.native="changeCheck" >
</el-checkbox>


js-code:

changeCheck: function() {
  this.actionsRedux("setChecked", !this.propFromReduxStore);
}

В компоненте может даже и не быть события click, а если и есть, то узнаем мы об его наступлении уже постфактум, не говоря уже об его подалении, но зато есть модификатор native, который позволяет получить доступ ко всем возможным нативным событиям. Так же есть модификатор stop и prevent, который даже позволит нам не писать e.stopPropagation() и e.preventDefault(), как это было с polymer. Малость, а приятно.
Вся суть в том: что при необходимости, мы можем получить доступ к нативным событиям компонента и полностью их подавить, и пустить data-workflow по нужному нам пути.

React не рассмотрел, так как на практике его не использовал, если не считать jsx-темплейтов в vue, но это совершенно не то. Но все-таки, насколько я наслышен от коллег по цеху, там таких проблем нет, в виду внутренней архитектуры обработки событий.

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


  1. mayorovp
    11.09.2017 08:30

    Ну и какой смысл сначала делать statefull компоненты, а потом разными трюками превращать их в stateless?


    Для связки с redux надо изначально разрабатывать компоненты, которые redux поддерживают. Такой компонент либо должен принимать допустимые действия (action) для изменения своего состояния как параметры, либо он должен иметь возможность объявить свои редьюсеры.


    1. kolesoffac Автор
      11.09.2017 10:25
      +1

      Речь идет не о самописных компонентах, а о сторонних компонентах разных ui(например: element, at-ui). В своих компонентах я могу организовать любой data-workflow, в сторонних компонентах глубоко в код не полезешь, верней можно, но не нужно, поэтому приходится искать пути работы с ними. И вот предлагаю одни из них.


      1. mayorovp
        11.09.2017 10:51

        Как правило, самое "вкусное" в сторонних компонентах — состояние и поведение, а не разметка. Выстраивая подобные костыли вокруг чужих компонентов, вы это все вынуждены переписать еще раз.


        Ну и в чем в таком случае смысл использования сторонних компонентов? Проще переписать их чем вот так делать.


  1. Odrin
    11.09.2017 10:30
    +1

    C vue немного другая ситуация, в нем нет как такагого одностороннего биндинга, как в polymer. Но подобную функциональность можно достигнуть через модификатор sync

    Вы сами-то читали документацию по приведенной вами ссылке? sync — это как раз сахар для имитации двустороннего биндинга. И в вашем примере он не нужен совсем.


    1. kolesoffac Автор
      11.09.2017 10:34

      А я чего возразил? Я сказал, что прямого одностороннего биндинга как в полимер, например, нет, а есть сахар в виде sync, который обеспечивает этот функционал.


      1. Odrin
        11.09.2017 10:37
        +1

        Биндинг во Vue только односторонний (как и в polymer, react, angular 2+). Sync — сахар для обеспечения двустороннего биндинга (как в angularjs). В вашем примере sync не нужен.
        P.S. может мы по разному понимаем термин «односторонний биндинг»?


        1. kolesoffac Автор
          11.09.2017 10:43

          Наверное. Двусторонний — когда изменение могут приходить в прибинденное проперти, как из вне, так и из самого контрола, то есть сам контрол меняет его значение в зависимости от тех или иных событий. И берет эту обработку полностью на себя, то есть про изменение мы узнаем когда изменение уже произойдет — прямое изменение проперти. Как раз этого нам надо избежать. Что бы в контрол данные приходили только из вне, а от контрола мы должны перехватить необходимые события и проксировать их в редак, а из него уже придет в сам контрол, это и есть односторонний бинд.


          1. Odrin
            11.09.2017 10:51
            +1

            Вот и я о том же. Однако с sync у вас какая-то путаница. Вот что говорит нам документация на этот счет:

            В некоторых случаях нам может понадобиться “двухсторонняя привязка” для входных данных — фактически, в Vue 1.x это было представлено модификатором .sync.

            … мы удалили модификатор .sync, когда была выпущена версия 2.0…

            В версии 2.3.0+ мы снова ввели модификатор .sync для входных данных, но на этот раз это просто синтаксический сахар, который автоматически преобразуется в дополнительный обработчик v-on


      1. mayorovp
        11.09.2017 10:54

        Читаем документацию по вашей же ссылке:


        В версии 2.3.0+ мы снова ввели модификатор .sync для входных данных, но на этот раз это просто синтаксический сахар, который автоматически преобразуется в дополнительный обработчик v-on:
        Следующее
        <comp :foo.sync="bar"></comp>
        будет преобразовано в:
        <comp :foo="bar" @update:foo="val => bar = val"></comp>

        Ну и зачем вам в вашем redux-way обработчик @update:foo="val => bar = val", который напрямую меняет стор? Куда правильнее будет написать этот @update:foo самому, засунув туда dispatch. Заодно можно избавиться от костылей в виде перехвата событий через e.stopPropagation().


        1. kolesoffac Автор
          11.09.2017 11:04
          -1

          Разве я где то описал обработчик update? Я предоставил вариант, который работает на практике, так, как нужно для схемы data-workflow redux'а. sync мне позволяет не синхронизировать значение с прибинденным проперти(понимаю что тавтология, название sync, а используем для того что бы не было синхронизации), а сохранение его во внутреннем стейте самого компонента. Соответсвенно при такой схеме работы мне просто не нужен никакой update.
          По поводу костылей, согласен, похоже очень на них, но мне не удалось никак заставить работать нужным образом без них. Буду благодарен любым наводкам на другие подходы.


          1. mayorovp
            11.09.2017 11:08

            Вы написали sync — а sync генерирует update, притом не тот что вам нужен. Зачем?


            Чем вас не устраивал вот такой вариант:


            <input :value="propFromReduxStore" @update:value="changeText"></input>


            1. kolesoffac Автор
              11.09.2017 11:16

              Генерирует, но он мне не мешает. Можно и таким вариантом, но чем он лучше чем с использование sync?

              <input :value.sync="propFromReduxStore" @change="changeText"></input>


              1. mayorovp
                11.09.2017 11:18

                Хорошо, а чем ваш вариант лучше вот такого:


                <input :value="propFromReduxStore" @change="changeText"></input>

                Неужели вы думаете, что компонент будет как-то по-особому вести себя просто из-за того что у него появился обработчик события?


                1. kolesoffac Автор
                  11.09.2017 11:23

                  Нет я так не думаю. Во-первых, я специально разделил на подразделы — работа с нативными контролами, и с компонентами, так как есть своя специфика обработки событий. То что вы написали выше это не компонент, а нативный контрол. И такой вариант не подойдет, по той причине, что все введенные данный сразу попытаются записаться напрямую в propFromReduxStore, а этого делать нельзя. Поэтому нам надо давить данное поведение, или sync или native.


                  1. mayorovp
                    11.09.2017 11:57

                    И такой вариант не подойдет, по той причине, что все введенные данный сразу попытаются записаться напрямую в propFromReduxStore, а этого делать нельзя.

                    Это вы из головы придумали или об этом можно прочитать где-то в документации?


                    1. kolesoffac Автор
                      11.09.2017 12:03

                      Из головы я ничего не придумываю. Все, что я описал это подходы, полностью работающие на практике, проверенные лично. А почитать например вот. Это обычный подход, как они сами пишут про себя, ненавязчивой реактивности. Но в данном случаи он нам не подходят.


                      1. mayorovp
                        11.09.2017 12:40

                        По вашей ссылке я не вижу ничего что подтверждало бы ваши слова.


                        1. kolesoffac Автор
                          11.09.2017 13:33

                          В чем именно Вы хотите убедиться в документации?


                        1. kolesoffac Автор
                          11.09.2017 13:45

                          Практические примеры, подтверждают мои слова. Они не теоретически описанные, а пройденные боевое крещение. И не из головы, а из конкретных проектов.


                          1. mayorovp
                            11.09.2017 13:59

                            Вот вам практический пример: https://jsfiddle.net/6wpesep0/2/


                            Из которого следует, что sync применительно к input.value вообще ничего не делает! Это опровергает ваши слова о том, что "прямого одностороннего биндинга нет" — он есть, и работает по умолчанию, без всяких модификаторов.


                            1. kolesoffac Автор
                              11.09.2017 14:28

                              Согласен, есть разница между v-model(двусторонний бинд) и :value(односторонний). И может не совсем удачный пример именно с инпутом. Но это не отменяет того, что есть компоненты, с которыми такое не пройдет без подавления внутренних ивентов. Например, jsfiddle.net/4zufmzvf, в этом примере прибинденное проперти меняется только непосредственно вручную, с подавлением ивентов, и в данном случаи уже без разницы это :value или v-model.


                              1. mayorovp
                                11.09.2017 14:33

                                И опять-таки, никакой .sync вам не потребовался. И даже e.stopPropagation() не потребовалось… Тогда о чем вообще ваш пост?


                                PS все три варианта: https://jsfiddle.net/4zufmzvf/1/


        1. kolesoffac Автор
          11.09.2017 11:08

          будет написать этот update:foo самому

          Согласен полностью, но речь идет об уже написанных не мной компонентах, со своим поведением update, и когда на уровне компонента мы узнаем об нем, то все уже свершилось, а нам надо проникнуть на самое начало этого события. А native позволяет мне перехватить событие в его самом начале, благодаря чему я могу им управлять так как мне надо, а не как реализовано внутри компонента.