Redux — пересмотр логики reducer'a и actions

  • Tutorial


На днях ковыряясь в множестве файлов redux'a, где по логике файлы вынесены в reducers, actions, константы типов actions. Bсе это оказалось весьма не простая задача держа все эти типы файлов у себя в голове и прослеживать логику. И… эврика, появилась идея упрощения написания redux логики. Возможно создавая свой велосипед, но кто не пытался писать свои велосипеды? Но главное это не написание а поддержка написанного когда. Давайте я вам немного постараюсь показать свое видение моей логики redux'a.


Начало


И так у нас есть reduce:


// импортируем константы
import { TODO } from './actions/const';
.....
// может быть ооочень много импортов 
....

// и наконец наш reducer
function todoApp(state = initialState, action) {
  switch (action.type) {
    case TODO.SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case TODO.ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    case TODO.TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
    })
   ...
  тут тоже может быть ооч много букв и вырастает до... ну в общем только поиск тебе поможет в этом лапшевидном файле
   ...
    default:
      return state
  }
}

взят из оф доки по redux.


action имеет вида типа:


// импортируем константы
import { TODO } from './const';

export const addTodo = (value) => ({
  type:  TODO.ADD_TODO,
  payload: value
})

константы я думаю нет необходимости показывать.


БООЛЬ


Попробую описать неистовство которое я испытываю читая код особенно при дебаге или расширении функционала.


  • поиск — нужно все время нажимать Ctrl + F причем глобально Ctrl + Shift + F
  • не видно сразу от куда ноги растут. Вытекает из пункта выше.
  • нет, это всего мало, так у меня еще весь проект пронизывают константы. Нет я не против констант но зачем? Тем более если их использовать вместе с вложенностью как в примере да если еще их конкатенировать из нескольких то это вообще ад навигации.
  • логика размазана. В одном месте действия в другом обработка этих действий в третьем (опционально) константы которые нужны только тем двум.
  • мне нужно при разработке или дебаге держать открытыми много файлов. Вытекает из пункта выше.
    ну и тп.

Вступление в логику


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


reducer


подсмотрено в google


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

То есть вал на нем есть шестерня эта шестерня передает другой шестерне вращение которая в свою очередь своему валу. Убираем вал и вместе с ним убирается шестерня. Не разрывный так сказать модуль.


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


action


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


И так поехали. Мой Велосипед


reducer:


export function todoApp(state = initialState, action) {
    if (typeof action.func === 'function') {
        return action.func(state);
    }
}

да это весь мой reducer. Щас возможно будет небольшой разрыв шаблонов, как? мы вынесли логику из reducer'a..? Да. скажу я вам, мы вынесли логику из reducer'a!!!


Давайте посмотрим на action:


export function addTodo = (value) => ({
    type:  'ADD_TODO' ,
    payload: value,
    func: (state) =>({...state, value})
  })
}

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


Стоит заметить Мы убрали константы. Да и switch тоже. Что позволило снизить сложность выполнения О(1) в reducer'e.


Это всего лишь пример-набросок который вы можете расширить и убрать combineReducers. Расширять, дополнять, изменять под свои нужды ведь это так здорово, брать инструмент и делать его идеальным для своих задач.


И главное хочется сказать.



Будьте умничками. бэээ

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

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

    +1
    Стоит заметить Мы убрали константы.

    Ну константы в `actions` у вас никуда не делись.

    Плюс, данный подход просто переложил проблему из одного места в другое. Вместо того что бы иметь большой `switch` с обработчиками в файле `reducer`, вы будете иметь уйму обработчиков в файле `actions`, в последствие, по мере разрастания проекта, по-прежнему придется пользоваться тем же поиском.

    У себя в проекты, мы пришли к тому, что бы стараться максимально комбинировать редьюсеры, если есть такая возможность, что бы отдельный редьюсер был минимально возможным.
      0
      будете иметь уйму обработчиков в файле `actions`, в последствие, по мере разрастания проекта, по-прежнему придется пользоваться тем же поиском.

      Ну будет как у вас с редусерами… Просто множество actions.
      Так что тут +-
      Про константы поправил. Спасибо. Была опечатка.
      –1

      Наблюдая выше в примере редьюсера комбинированное использование как Object.assign так и спред оператора, подозреваю что и остальная часть проекта страдает от подобной неконсистентности, которая и вызывает вашу так называемую "боль"

        +1
        Пример с Object.assign взят из оф доки по redux
          –1
          Вот вот. И вообще лучше использовать спред оператор, тк Object.assign с TypeScript не очень хорошо дружит (можно легко неконтролируемых дел натворить).
          +2
          поиск — нужно все время нажимать Ctrl + F причем глобально Ctrl + Shift + F
          Используйте TypeScript.
          не видно сразу от куда ноги растут. Вытекает из пункта выше.
          Используйте TypeScript.
          нет, это всего мало, так у меня еще весь проект пронизывают константы. Нет я не против констант но зачем? Тем более если их использовать вместе с вложенностью как в примере да если еще их конкатенировать из нескольких то это вообще ад навигации.
          Используйте github.com/pelotom/unionize.
          логика размазана. В одном месте действия в другом обработка этих действий в третьем (опционально) константы которые нужны только тем двум.
          Используйте github.com/pelotom/unionize.
          Вот ради этого мы вынесли логику отвечающую за передачу данных стору. Reducer остался обеспечивать работу всего механизма. И он должен делать это хорошо не отвлекаясь на вещи его не касающиеся. А нам остается только наблюдать порядок в том от куда растут ноги и если надо то быстро найти и исправить или дополнить.
          Очень недальновидно прилепить реализацию ридьюсеров к экшенам:
          — В приложении может быть много разных ридьюсеров на тот же самый тип экшена. Допустим я послал экшен из модуля1, а модуль2 и модуль4 будут по-своему его обрабатывать собственными ридьюсерами, плюс lazy модуль5 когда загрузится тоже начнет этот экшен обрабатывать. Пример утрирован, но что-то такое может существовать. Исходите из посыла что экшен это событие с пейлоадом, а ридьюсер это обработчик события. Вы ведь не станете спорить что у события может быть сколько угодно разных обработчиков.
          — Представьте себе что у вас больше SPA со множеством lazy/ленивых модулей. Любой экшен может быть послан из любого модуля при этом сами экшены очень легковесны. То есть это не так страшно импортировать допустим все экшены или по отдельности в каждый модуль (особенно если это константы, а не бандлы тк tree shaking). Но вы привязали сам обработчик к экшену и в итоге все модули загрузят и всю логику ридьюсеров тоже что не является желательным поведением приложения.
          — Я бы еще причин придумал, но написанного считаю уже достаточно.
            –1
            Вот у вас есть state. Большой, развесистый, с кучей комбинированных редюсеров. У вас или будет 2 куска обработки одного события в разных файлах с разными редюсерами, и потом из результатов их работы будет собираться единый стейт, или же будет один метод, в котором будет описано полное преобразование стейта в одной функции. В redux традиционный подход с switch-case — просто неплохо работающий вариант. Требование к его реализации только одно — newState = reducer(oldState, action). А внутри можно и к dom обращаться, и к сети, и 3d рисовать. Хоть и не стоит.

            В традиционной реализации redux есть проблема. И если многие считают проблемой большое количество бойлерплейта и констант, то я считаю проблемой линейную сложность редюсеров от количества экшнов. Если у тебя есть 500 разных типов action, то store.dispatch каждый раз будет делать 500 сравнений ссылок на строку action type

            А «Используйте TypeScript» это глупый максимализм. Несмотря на мои определенные к нему симпании, typescript и javascript — разные языки программирования. Вы ведь не приходите в посты про python и не говорите там «Используйте C#»?
              +2
              то я считаю проблемой линейную сложность редюсеров от количества экшнов.
              Проблема скорее не в количестве а как часто экшены летают. Оптимизировать резолвинг ридьюсера по типу экшена всегда можно.
              А «Используйте TypeScript» это глупый максимализм.
              Это насущная необходимость при разработке решений сложнее hello world, просто не все это еще осознали.
              typescript и javascript — разные языки программирования.
              Это не так, в рантайме они абсолютно идентичны.
              Вы ведь не приходите в посты про python и не говорите там «Используйте C#»?
              А вот как раз python и C# совсем разные.
                0

                Кажется у нас появилась новая религия. Свидетели TypeScript-а :)

                  0
                  Каждому свое. Мне эту штуку никто не назязывал и выбор был не спонтанным, сначала попробовал некоторое время.
                +2
                И если многие считают проблемой большое количество бойлерплейта и констант, то я считаю проблемой линейную сложность редюсеров от количества экшнов. Если у тебя есть 500 разных типов action, то store.dispatch каждый раз будет делать 500 сравнений ссылок на строку action type

                const ACTION_HANDLERS = {
                  [ACTION_1]: (state, action) => ...,
                  [ACTION_2]: (state, action) => ...,
                  [ACTION_3]: (state, action) => ...,
                  [ACTION_4]: (state, action) => ...,
                  ...
                };
                
                const reducer = (state = INITIAL_STATE, action) => {
                  const handler = ACTION_HANDLERS[action.type];
                  return handler ? handler(state, action) : state;
                };
                  0

                  Это не очень сильно отличается от предложенного в статье, как мне кажется :)

                    +1

                    Нет. Отличается сильно. Этот весьма распространённый подход полностью аналогичен традиционному решению из документации, но вместо switch-case по экшенам используется объект, т.е. фактически хеш-таблица. Сложность такого подхода — O(1) в отличие от switch, которое потенциально может быть O(n).


                    В статье же предложен вариант, когда редьюсер фактически передаётся в экшене. Ничего общего с моим вариантом он не имеет.

              +9

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

                0
                Меня тоже всегда убивало это месиво из switch-case
                Решение классное, простое и грамотное. Но в традиционном стиле redux его бы надо было написать как middleware
                Что-то в виде:
                const functionalReducerMiddleware = store => next => action => {
                    if (action.func) return action.func(next)
                    else return next
                }
                

                Хотя вообще я бы если интегрировал такое решение, как-то гарантировал бы, что action.type у таких штук будет уникальный и больше нигде не используемый, чтобы ни у кого не пришло в голову обработать такой экшн ещё и в редюсере
                  0
                  Меня тоже всегда убивало это месиво из switch-case

                  Для меня одна из загадок вселенной состоит в том, на кой чёрт все пишут эти switch-case-ы? Кто мешает выносить каждый case в отдельный метод а оркестрировать их по action.type + hashMap. Собственно когда мы декомпозируем обыкновенный код — мы же не пишем повсюду switch-case-ы. Они ж уродливы.


                  Народ посмотрел документацию на redux и пошёл копипастить.

                    0
                    Народ посмотрел документацию на redux и пошёл копипастить.
                    Кто-то еще спорит что большая часть JS пользователей не скрипт кидди.
                  0
                  redux-actions в помощь
                    0

                    Подход интересный, но константы всё-равно нужны. Экшены же как-то надо диспэтчить.

                      0
                      Не нужны. Вот хелпер github.com/pelotom/unionize и также существуют много аналогов.
                        0

                        Я правильно понимаю, что unionize рушит все вкусности на уровне import-export-а? Т.е. мы работаем не точечно с конкретным actionCreator-ом, когда импортируем что-то, а тащим весь список экшнов и на месте используем нужное? Если да, то я бы не стал такое применять в деле на проектах без TypeScript-а. Мне кажется выгода графа зависимостей превалирует над таким подходом.

                          0
                          Экшены это очень легковесные штуки, будь то константы или бандл констант. Да unionize проявляет себя лучшим образом именно при использовании TypeScript. Сама библиотека совсем легковесная и грузится один раз в vendor бандле, а бандлы экшенов тоже не тяделые совсем. Именно бандлы, логическое деление, тк пихать все в один бандл и потом его везде импортировать было бы неразумно.
                            0

                            Я имел ввиду граф зависимостей а не "вес" кода. В случае TS я могу не переживать, что использую ключ action-а, которого нет. В случае JS такую возможность предоставляет простой import конкретного action-а (что кажется вообще не про unionize). Дальше дело за линтерами.

                              0
                              Очевидно это так. Но те кто до сих пор работают с чистым JS ведь согласны все держать в голове, это их путь и ничего нового или неожиданного здесь нет.
                                0

                                Ну собственно отдельно лежащие action-type от которых у всех так бомбит — это ведь как раз попытка Дена Абрамса не держать это в голове. Своего рода типизация через import-export-ы )
                                Я смотрю в экосистеме React в целом любят делать "типы" на JS руками.

                                  0
                                  Это полумера тк в масшате всего приложения редакс экшены это небольшая доля кода. К томе же это добавляет бойлерплейта.
                        0

                        Есть 1 интересное решение с константами, которое я задействовал в одном из своих проектов. Опишу только суть:


                        // action creators
                        export const aOpen = new ActionBuider('id');
                        export const aSave = new AsyncActionBuilder(['id', 'data'], asyncHandler);
                        
                        // reducers 
                        const handlers = 
                        {
                         [aOpen]: (st, action, rootSt) => { ... },
                         [aSave]: (st, action, rootSt) => { ... },
                        };
                        
                        // somewhere
                        connect(
                         mapStateToProps,
                         { open: aOpen }
                        )(SomeComponent)

                        Может возникнуть вопрос — а где вообще типы? а вот они: aOpen, aSave. Самописный babel-plugin просто меняет new ActionBuilder(...).setType('OPEN');. Если его не подключить, то будет просто уникальный ID аля "id1231". Если подключить — обыкновенные привычные типы. Ну и систему префиксов вдобавок (в примере не стал пудрить мозг с ними)

                        0
                        Мне пока что сложно представить как повлияет такое нововведение на реальный проект. Пользуясь случаем хочу дать ссылку на интересное предложение которое я например уже точно использую и которое кажется облегчает разработку github.com/erikras/ducks-modular-redux
                          0

                          Рекомендую взглянуть на его развитие https://github.com/alexnm/re-ducks

                          0
                          ссылку на интересное предложение

                          Эта концепция датируется аж 2015 годом и уже чуть ли не промышленный стандарт :)

                          +1

                          При вашем подходе экшны более не сериализуемы. Т.е. вам более не смогут прислать пачку экшнов с продавцом, вы у себя сделаете риплей и восстановите стейт.
                          Второй момент: мне не очень понятно как с таким подходом вы разбиваете стейт на более маленькие кусочки. Учитывая, что вы предлагаете выкинуть combineReducers, получается каждый экшн должен обрабатывать весь стейт целиком?

                            0
                            Получился github.com/developit/unistore
                            Легкий аналог redux/react-redux для react/preact от разработчика preact.

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

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