image


Это перевод статьи "What’s So Great About Redux?" (автор Justin Falcone), которая мне показалась весьма приятной и интересной для прочтения, enjoy!


Redux мастерски справляется со сложными взаимодействиями состояний, которые трудно передать с помощью состояния компонента React. По сути, это система передачи сообщений, которая встречается и в объектно-ориентированном программировании, но она не встроена непосредственно в язык, а реализована в виде библиотеки. Подобно ООП, Redux переводит контроль от вызывающего объекта к получателю — интерфейс не управляет состоянием напрямую, а передает ему сообщение для обработки.


В этом плане хранилище в Redux — это объект, редюсеры — это обработчики методов, а действия — это сообщения. Вызов store.dispatch({ type:"foo", payload:"bar" }) равносилен store.send(:foo, "bar") в Ruby. Middleware используется почти таким же образом, как в аспектно-ориентированном программировании (например, before_action в Rails), а с помощью connect в react-redux осуществляется внедрение зависимости.


Какие преимущества даёт такой подход?


  • Благодаря инверсии контроля, упомянутой выше, исчезает необходимость обновлять пользовательский интерфейс, как только меняется имплементация смены состояний. Добавлять такие сложные функции, как логирование, отмена действия или даже time-travel debugging, становится проще простого. Интеграционные тесты сводятся лишь к тому, чтобы проверить, отправляется ли правильное действие, а для всего остального достаточно юнит-тестов.


  • Состояние компонентов в React слишком неповоротливо для работы со сквозной функциональностью, которая затрагивает многие модули приложения, как, например, информация о пользователе или оповещения. Как раз для этого в Redux есть дерево состояний, независимое от пользовательского интерфейса. К тому же, при обработкe состояния вне интерфейса легче поддерживать постоянство, ведь сериализация в localStorage или URL проводится в единственном месте.


  • Редюсеры дают огромную свободу в работе с действиями, которые можно сочетать, отправлять одновременно и даже обрабатывать в стиле method_missing.

Но все эти случаи особые. Что же насчёт более простых сценариев?

Как раз здесь у нас проблемы.


  • Actions можно рассматривать как сложные переходы между состояниями, но в основном они лишь задают одно значение. В приложениях, сделанных на Redux, зачастую накапливается множество таких простых actions, и это явно напоминает Java с написанием функций сеттеров вручную.


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


  • Функции-редюсеры могли бы справиться с самыми замысловатыми задачами метапрограммирования, но обычно сводятся к примитивной диспетчеризации action согласно его типу. Это не проблема для таких языков, как Elm или Erlang, которые отличаются лаконичным и выразительным синтаксисом pattern matching, но в Javascript приходится иметь дело с громоздкими конструкциями switch.

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


Легко списать всё это на человеческий фактор: не осилил документацию, или "мастер глуп — нож туп" — но такие проблемы встречаются подозрительно часто. Не в ноже ли проблема, если он туп для большинства мастеров?


Получается, лучше сторониться Redux в обычных случаях и приберечь его для особенных?


Как раз такой совет вам даст команда Redux — и я говорю то же самое своим коллегам: не беритесь за него до тех пор, пока setState не начнёт совсем выходить из-под контроля. Но даже я сам не следую собственным правилам, потому что всегда можно найти повод использовать Redux. Может быть, у вас есть много actions вроде set_$foo, при этом с каждым присвоением значения обновляется URL или сбрасывается ещё какое-нибудь промежуточное значение. Может, у вас установлено чёткое однозначное соответствие между состоянием и пользовательским интерфейсом, но вам требуется логирование или отмена действий.


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


Что же делать в таком случае?


К счастью, Redux достаточно гибкий, чтобы подключить к нему сторонние библиотеки для работы с простыми вещами — тaкие, как Jumpstart (https://github.com/jumpsuit/jumpstate). Поясню: я не считаю, что Redux нельзя использовать для низкоуровневых задач. Просто если работа над ними отдаётся сторонним библиотекам, это усложняет понимание, и по закону тривиальности время тратится на мелочи, потому что каждому пользователю в итоге приходится строить собственный фреймворк по частям.


Некоторым такое по душе


И я в их числе! Но это относится далеко не ко всем. Я большой поклонник Redux и использую его почти во всех проектах, но мне также нравится пробовать новые конфигурации webpack. Я не типичный пример пользователя. Создание собственных абстракций на основе Redux даёт мне новые возможности, но что могут дать абстракции, написанные каким-нибудь Senior-инженером, который не оставил никакой документации и уволился полгода назад?


Вполне возможно, что вы и не встретитесь со сложными проблемами, которые так хорошо решает Redux, особенно если вы новичок (junior) в команде, где такими заданиями занимаются старшие коллеги. Тогда вы представляете себе Redux как такую странную библиотеку, с которой всё переписывают по три раза. Redux достаточно прост, чтобы работать с ним машинально, без глубокого понимания, но радости и выгоды от этого мало.


Так я возвращаюсь к вопросу, который задал ранее: если большинство использует инструмент неправильно, не в нём ли вся проблема? Качественный инструмент не просто полезен и долговечен — с ним ещё и приятно работать. Использовать его правильно удобнее всего. Такой инструмент делается не только для работы, но и для человека. Качество инструмента — это отражение заботы его создателя о мастере, который будет им пользоваться.


А как мы заботимся о мастерах? Почему мы утверждаем, что они всё делают не так, вместо того, чтобы поработать над удобством инструмента?


В функциональном программировании есть похожее явление, которое я называю «Проклятием урока о монадах»: объяснить, как работают монады, проще простого, а вот рассказать, в чём их польза, на удивление трудно.


Ты серьёзно собираешься объяснять монады посреди поста?


Монады — это распространённая в Haskell абстракция, которая используется для самых разных вычислений, например, при работе со списками или обработке ошибок, состояний, времени или ввода/вывода. Синтаксический сахар в виде do-нотации позволяет представлять последовательности операций над монадами в форме, похожей на императивный код, примерно как генераторы в JavaScript, которые делают асинхронный код похожим на синхронный.


Первая проблема состоит в том, что не совсем правильно описывать монады с точки зрения их применения. Монады появились в Haskell для работы с побочными эффектами и последовательным вычислением, но в абстрактном смысле они не имеют с этим ничего общего: они просто представляют из себя набор правил взаимодействия двух функций, и в них не заложено никакого особого смысла. Похожим образом ассоциативность применима к арифметике, операциям над множествами, объединению списков и null propagation, но существует независимо от них.


Вторая проблема — это многословность, а значит, как минимум, визуальная сложность монад по сравнению с императивным подходом. Четко определенные опциональные типы вроде Maybe более безопасны, чем поиск подводных камней в виде null, но код с ними длиннее и несуразнее. Обработка ошибок с помощью типа Either выглядит понятнее, чем код, в котором где угодно может быть брошено исключение, но, согласимся, код с исключениями намного лаконичнее, чем постоянный возврат значений с Either. Что касается побочных эффектов в состоянии, вводе/выводе и т.д., то они и вовсе тривиальны в императивных языках. Любители функционального программирования (я в их числе) возразили бы, что работать с побочными эффектами в функциональных языках очень легко, но вряд ли кого-то получится убедить, что какое бы то ни было программирование — это легко.


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


Так к чему же всё это?


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


Этим пониманием нелегко поделиться, потому что основные принципы сводятся к банальным аксиомам («избегайте побочных эффектов») или настолько абстрактны, что почти теряют смысл ((prevState, action) => nextState). Конкретные примеры не помогают: они только демонстрируют многословность Redux, но не показывают его выразительность.


Достигнув просвещения, многие из нас сразу же забывают о том, как к нему пришли. Мы уже не помним, что наше «просвещение» — это результат многократных ошибок и заблуждений.


И что ты предлагаешь?


Я хочу, чтобы мы осознали, что у нас есть проблема. Redux простой, но не лёгкий. Это оправданное решение разработчиков, но в то же время и компромисс. Многим людям пригодился бы инструмент, который бы частично пожертвовал простотой механизма в пользу простоты использования. Но в сообществе зачастую даже не признают, что какой-то компромисс вообще был сделан.


По-моему, интересно сравнивать React и Redux: хоть React и гораздо сложнее Redux и его API намного шире, как ни странно, его легче изучать и использовать. Всё, что действительно необходимо в API React, — это функции React.createElement и ReactDOM.render?, а с состоянием, жизненным циклом компонентов и даже с событиями DOM можно справиться по-другому. Включение всех этих функций в React сделало его сложнее, но при этом и лучше.


"Атомарное состояние" — это абстрактная идея, и оно приносит практическую пользу только после того, как вы в нем разберётесь. С другой стороны, в React метод компонента setState управляет атомарностью состояния за вас, даже если вы не понимаете, как оно работает. Такое решение не идеально — было бы эффективнее отказаться от состояния вообще, или в обязательном порядке обновлять его при каждом изменении. Ещё этот метод может преподнести неприятные сюрпризы, если вызвать его асинхронно, но всё же setState намного полезнее для React в качестве рабочего метода, а не просто теоретического понятия.


И команда разработчиков Redux, и сообщество его пользователей активно выступают против расширения API, но склеивать десятки маленьких библиотек, как это делается сейчас, — утомительное занятие даже для экспертов и тёмный лес для новичков. Если Redux не сможет вырасти до уровня встроенной поддержки простых случаев, ему потребуется фреймворк-«спаситель», который займёт эту нишу. Jumpsuit мог бы стать неплохим кандидатом — он воплощает идеи действий и состояния в виде конкретных функций, при этом сохраняя характер отношений «многие ко многим». Но выбор спасителя не так важен, как сам факт спасения.


Вся ирония в том, что смысл существования Redux — это «Developer Experience»: Дэн разработал Redux, чтобы изучить и воспроизвести time-travel debugger как в Elm. Однако, по мере того, как идея приобретала собственную форму и превращалась, по факту, в объектно-ориентированную среду экосистемы React, удобство «DX» отошло на второй план и уступило гибкости конфигурации. Это вызвало расцвет экосистемы Redux, но там, где должен быть удобный фреймворк с активной поддержкой, пока зияет пустота. Готовы ли мы, сообщество Redux, заполнить её?


Перевод выполнен при содейтсвии Olga Isakova

Поделиться с друзьями
-->

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


  1. raveclassic
    21.07.2017 13:29
    +5

    Ну, понеслась!


  1. vasIvas
    21.07.2017 13:40
    -4

    Радует что новички в JavaScript с JQ переключились на react.


    1. asci
      21.07.2017 15:25
      +1

      скорее на ангуляр, реакт сложнее в и старте и в продолжении


      1. inoyakaigor
        21.07.2017 16:21

        Ну насчёт продолжения я не совсем согласен. Концепция Реакта проще, нет никаких посторонних сущностей а-ля сервисы, модели, всякая подкапотная магия и ngIf и т.п. Стоит лишь осознать как писать правильный JSX и ещё пару правил и всё становится просто.


        1. Druu
          21.07.2017 17:11

          > Стоит лишь осознать как писать правильный JSX и ещё пару правил и всё становится просто.

          На ангуляре точно так же — разобрались с темплейтами и поехали. DI вас использовать никто не заставляет, особенности change detection'а проблем не доставят (если выполнять пару правил — точно тех же, к слову, как и в реакте :))
          Что там еще подкапотного остается?


          1. justboris
            21.07.2017 18:11
            +4

            Как рендерится Реакт:


            const myElement = React.createElement(MyElement)
            const appNode = document.getElementById('my-app')
            ReactDOM.render(myElement, appNode)

            мы выдаем Реакту DOM-ноду, он выводит в нее свой контент. Все просто и понятно.


            Как рендерится Angular:


            @NgModule({
              imports: [
                BrowserModule,
                FormsModule
              ],
              declarations: [
                AppComponent
              ],
              bootstrap: [ AppComponent ]
            })
            class AppModule {}
            
            platformBrowserDynamic().bootstrapModule(AppModule)

            Что здесь происходит? Куда в итоге Angular отрендерит контент? Что делать, если на странице одновременно два приложения? Ничего не понятно.


            Возможно, на это можно найти ответы в документации, но подкапотной магии тут немало.


            1. Akuma
              21.07.2017 18:47

              Позвольте немного дополнить.

              React — все же по сути шаблонизатор. И за счет этого его гораздо проще понять. Он просто рендерит компонент. Все, точка. Причем все компоненты не связаны друг с другом и им плевать друг на друга.

              В ангуляре очень большая связанность, на мой взгляд (я работал только с первым правда, но там ад вообще), и именно это порождает тонну проблем вида «боремся с фреймворком».


              1. Druu
                21.07.2017 20:54

                > Он просто рендерит компонент.

                Так можно про что угодно сказать.


            1. Druu
              21.07.2017 20:47

              > Как рендерится Angular:

              Этот код не эквивалентен коду на реакте. Эквивалентный код в ангуляре написать нельзя: ангуляр ничего не знает о дом и браузере. По-этому вы не можете ни создать дом-ноду, ни отрендерить ее в дом-ноду (при запуске, например, на декстопе, вне браузера, никакого дома может не быть, по--этому можно считать, что его по факту и нет).


              1. justboris
                21.07.2017 21:05
                -1

                Прошу прощения, вместо слова "рендерится" надо было использовать слово "инициализируется".


                Оба фрагмента кода эквивалентны: они инициализируют корневой компонент вашего приложения.


                1. Druu
                  21.07.2017 21:34

                  > Оба фрагмента кода эквивалентны: они инициализируют корневой компонент вашего приложения.

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


      1. bustEXZ
        21.07.2017 16:22
        +1

        Не знаю, если говорить об ангуляре 2м, то для старта он гораздо сложнее. Не знаю как вы сравниваете :)

        PS. raveclassic Понеслась!)


      1. vintage
        23.07.2017 11:28

        Всё зависит от амбиций. Если хочется писать меньше, а делать больше, то ставка ставится на какой-либо фреймворк, где всё уже продумано/написано более компетентными ребятами (Angular, например, а то и ExtJS). Если хочется чувствовать себя крутым (и зарабатывать соответственно), то велосипедят на самых модных технологиях и инструментах (сейчас это React+Redux+Babel+WebPack).


      1. Odrin
        24.07.2017 11:35

        Angular проще в старте, чем React? Достаточно открыть документации обоих проектов что бы убедиться в обратном.


        1. Druu
          24.07.2017 12:33

          Ангуляр проще для бэкендщиков.


        1. vintage
          24.07.2017 15:09
          +2

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


          1. justboris
            24.07.2017 15:53

            десяток библиотек

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


  1. gotoxy
    21.07.2017 17:11

    В чём сила, брат? — В переменной типа bool.


  1. TheShock
    21.07.2017 22:39
    +3

    В чём сила Redux?

    Статьи про редакс для новичков — это как типичный неудачный форсед-мем. Всем уже тошно от одного заголовка, а они все лезут и лезут.


    1. RubaXa
      22.07.2017 13:48

      Тогда Redux with Ramda вам точно должно понравиться ;]


  1. Druu
    21.07.2017 23:19

    Раньше было модно писать статьи про монады. Сейчас стало модно писать статьи про редакс. А автор, видимо, решил хайпануть сразу с обеих тем?


  1. Legion21
    22.07.2017 16:52

    Спасибо за статью!


  1. PFight77
    22.07.2017 20:41
    +6

    Ответа на вопрос "в чем сила" так и не прочитал. Сказано что 1) Redux сложный 2) неимоверно крутой (без объяснений) 3) никто его не понимает 4) у него скудный неудобный API 5) был придуман time-travel и всякий мидл-вар в нем удобен. В чем сила так и не раскрыто.


  1. i360u
    24.07.2017 10:17
    +1

    По сути, единственная проблема, которую решает Redux — это проблема "эталонной версии данных", когда из множества источников изменений нужно получить один непротиворечивый "стейт" для формирования текущего "вью". Эффективных решений у этой проблемы может быть сотня, но Redux (а точнее Flux) — это решение хоть как-то формализует и позволяет неопытным разработчикам не наступать на грабли (но делает это не всегда оптимальным образом, это не "серебряная пуля"). Еще ни разу не встречал статей про Redux, где об этом было бы сказано простым языком: все постоянно сбиваются в какую-то малопонятную и ненужную заумь, как и в этой статье.


    1. vintage
      24.07.2017 11:01
      +2

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


      1. i360u
        24.07.2017 11:23

        "позволяет" != "вынуждает". Стрелять себе по ногам — святое право каждого.


  1. megahertz
    24.07.2017 11:58

    У меня с Redux вышло как с Angular 1 в свое время. Почитал, вроде красиво, но что-то отталкивает. После огромного роста популярности возникает мысль, что может я старею и пора переучиваться? Пробую на парочке простых проектов, попутно лучше разбираясь с идеологией. Приложения работают, после пары крупных рефакторингов даже код смотрится хорошо, но большой симпатии к инструменту так и не возникает. Angular 1 был заменен на React, который меня очень радует. Вместо Redux часто прибегаю к mobx, но не могу сказать, что он меня полностью устраивает.


    1. vintage
      24.07.2017 15:10

      А в чём не устраивает?


      1. megahertz
        24.07.2017 15:51
        +2

        Затрудняюсь точно сказать, это больше субъективное ощущение, возникающее когда выбираю инструменты для нового проекта. Из того что явно замечаю — шаблонный код и размазывание логики по разным action, в результате чего теряется ощущение целостности этой логики.


  1. comerc
    25.07.2017 15:14
    -2

    Я пришёл к такому разделению редюсеров. Есть общий редюсер app, где свалка глобальных состояний приложения. Для каждой сущности два редюсера: item и list. При этом редюсер для item используется на нескольких страницах (просмотр и редактирование сущности). При этом данные отображаемых сущностей в списке (list) спрятаны в локальных стейтах, а в редюсерах живут флаги и сайд-эффекты. И только данные для item так же живут в редюсере, потому что их нужно отображать в форме и в превью (например). Благодаря redux-act, ни один switch не пострадал.


    Чего тут сложного?!
    // @flow
    import { createAction, createReducer } from 'redux-act'
    import api from 'api'
    
    export const REDUCER = 'user'
    const NS = `@@${REDUCER}/`
    
    const reset = createAction(`${NS}RESET`)
    const set = createAction(`${NS}SET`)
    
    export const readItem = (id: ?number) => (dispatch: Function, getState: Function) => {
      if (id) {
        return api.get(`/api.php/members/${id}`).then(response => 
          dispatch(set(response.data))
        )
      }
      dispatch(reset())
      return Promise.resolve()
    }
    
    export const submit = (id: ?number, values: Object) => (dispatch: Function, getState: Function) => {
      if (id) {
        return api.put(`/api.php/members/${id}`, values)
      }
      return api.post(`/api.php/members`, values)
    }
    
    const initialState = {}
    
    export default createReducer(
      {
        [reset]: () => ({ ...initialState }),
        [set]: (state, data) => ({ ...state, ...data }),
      },
      initialState,
    )