redux-refine — простая радость перфекциониста

image


Скажите, люди, я один испытываю небольшой душевный зуд
от необходимости писать нечто вот эдакое? :


export const ADD_TODO = 'ADD_TODO'
export const DELETE_TODO = 'DELETE_TODO'
export const EDIT_TODO = 'EDIT_TODO'
export const COMPLETE_TODO = 'COMPLETE_TODO'
export const COMPLETE_ALL = 'COMPLETE_ALL'
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'

Я почему то думаю, что нет и иногда встречая в чьём то коде


if (action.type === ADD_TODO) {
  // ...
}

вместо ядрёного switch — case, я понимаю, что не единственный такой я на свете перфекционист, страдающий от этого "чуть-чуть не так как надо" в классическом Redux


Если Вам, уважаемый читатель, знакома эта боль, возрадуйтесь! под катом есть лекарство всего в две строчки кода :)


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


По сути дела, dispatch — это метод Store, аналогичный по смыслу методу emit старого доброго EventEmitter и в терминах классической событийной модели, у нас фактически Store подписан на события, имена которых называются типами экшенов и которые принято задавать в виде вышеупомянутых констант, в связи с чем у меня постоянно возникал вопрос, почему я должен хранить это где то отдельно, да к тому же повторно прибегая к такому нелепому дублированию кода? Исходная мысль то ясна, нам необходимо подстраховаться от конфликтов и обеспечить некоторую консистентность между экшенами и редюсерами, но не уже ли нельзя сделать это как то элегантней?


Я понимаю, что люди разные и если у кого то возникнет аргументированное возражение на этот мой лёгкий дискомфорт от работы с кодом Redux, буду рад выслушать любые мнения в комментариях, но тем, кто разделяет сие чувство, позвольте представить redux-refine


Идея в основе проста:


Я предлагаю использовать вместо switch-case хэш, индексированный типом экшенов, так как в объекте не может быть одинаковых свойств, что исключает конфликты в рамках одного редьюсера, а так же позволяет экспортировать типы экшенов для модуля, из которого они диспатчатся


Так же, такой подход обеспечивает чистую связность кода, следующую логике one way binding и отражающему направление потока данных в приложении, а именно:
мы видим в компоненте, методы какого модуля с экшенами он использует, а в модуле с экшенами мы видим, каким редюсерам он отправляет экшены.


По просьбе tmnhy на наглядном примере поясню:


в экшенах мы делаем так:


import { actionTypes as types1 } from 'reducers/reducer1'
import { actionTypes as types2 } from 'reducers/reducer2'

const { ACTION_1_1, ACTION_1_2, ACTION_1_3 } = types1
const { ACTION_2_1, ACTION_2_2, ACTION_2_3 } = types2

в редюсерах так:


reducer1:


import { getActionTypes, connectReducers } from 'redux-refine'

export const initialState = {
  value1: 0,
  value2: '',
  value3: null,
}

const reducers = {
  ACTION_1_1: (state, {value1}) => ({...state, value1}), 
  ACTION_1_2: (state, {value2}) => ({...state, value2}), 
  ACTION_1_3: (state, {value3}) => ({...state, value3}), 
}

export const actionTypes = getActionTypes(reducers)
export default connectReducers(initialState, reducers)

reducer2:


import { getActionTypes, connectReducers } from 'redux-refine'

export const initialState = {
  value1: 0,
  value2: '',
  value3: null,
}

const reducers = {
  ACTION_2_1: (state, {value1}) => ({...state, value1}), 
  ACTION_2_2: (state, {value2}) => ({...state, value2}), 
  ACTION_2_3: (state, {value3}) => ({...state, value3}), 
}

export const actionTypes = getActionTypes(reducers)
export default connectReducers(initialState, reducers)

в том месте, где Вы предпочитаете комбинировать редюсеры всё по прежнему:


import { combineReducers } from 'redux'

import reducer1, { initialState as stateSection1 } from './reducer1'
import reducer2, { initialState as stateSection2 } from './reducer2'

export const intitialState = {
  stateSection1, stateSection2
}

export default combineReducers({
  stateSection1: reducer1,
  stateSection2: reducer2
})

Да, конечно я понимаю, что это весьма мелочное нововведение, но мне от такого стиля работать с кодом на много приятней :)


И пожалуйста, не судите строго, если что — это мой первый пост на хабре


UPD:


Выхватив множество комментариев, интереснейших, но высказанных с разной долей недопонимания о том, что это вообще такое — redux-refine, я решил добавить ещё более детальное разъяснение:


Вот что я сделал:


1 Заменил конструкцию switch-case на выбор по ключу в хэшэ:


это


function reducer(state, {type, data}){
  switch(type) {
    case 'one': return {...state, ...data};
    case 'two': return {...state, ...data};
    case 'three': return {...state, ...data};
    default: return state;
  }
}

заменил на это


function reducer(state, {type, data}){
  return ({
    one: {...state, ...data},
    two: {...state, ...data},
    three: {...state, ...data}
  })[type] || state;
}

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


const reducers = {
  one: (state, data) => ({...state, ...data}),
  two: (state, data) => ({...state, ...data}),
  three: (state, data) => ({...state, ...data})
}

function reducer(state, {type, data}){
  return (redusers[type] || (state => state))(state, data);
}

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


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


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

Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 44
  • 0
    Идея в основе проста:

    И где конкретика, примеры кода?
    • 0
      В репозитории по ссылке на проект есть, но видимо действительно надо было сдублировать в статью. Просто идея слишком проста, как далее и пишет webdevium и я решил её не перегружать
      • +1
        Просто идея слишком проста

        Без примеров неполноценно, а лезть из статьи на сторонний ресурс, чтобы понять что да как, совсем плохо. В принципе, тогда после преамбулы можно было сразу ссылку на репозиторий дать и всё.
        • 0
          Добавил пояснение, спасибо за подсказку
    • +3
      Эх. И на кой эта статья? Неужели кто-то, понимающий в языке, не знает, что можно использовать словари?
      • +1
        Поймите правильно, я не сомневаюсь в профессионализме кого-то понимающего, но всё равно ведь конкретно в этом случае практически везде используются подходы
        компонент -> экшены < — константы -> редьюсеры
        или
        компонент -> экшены < — редьюсеры
        вместо логически правильного
        компонент -> экшены -> редьюсеры
        где стрелкой показано, откуда что импортируется
        • 0
          Понимаю правильно, естественно.
          Но проблема более фундаментальная — слепое наследование кода, который начинающие разработчики копируют у таких же начинающих разработчиков, которые прочли 2-3 страницы из книги и бегут писать статьи с громкими криками «так правильно. просто выключайте мозги. копируйте этот код».
          • +1
            Согласен, но смириться с положением вещей не в силах, уж простите )))
        • –1

          Действительно, а на кой нужна азбука? Неужели читающий слова не знает букв?!


          Кстати, дискуссия под этой "ненужной" статье вполне себе годная. Может для этого?

        • +1
          По-большому счету вы изобрели заново redux-actions, с той лишь разницей, то намертво связали имена констант с их значениями. Это плохо потому, что для констант обычно используются довольно короткие имена. Есть очень не маленькая вероятность, то при таком походе при подклюении очередной библиотеки ваши типы экшнов совпадут, и приехали. Решение было предложено в весьма популярном пропосале ducks-modular-redux
          • +1
            Ну, вообще то в классической концепции, разные редюсеры теоретически могут реагировать на один и тот же экшн, обновляя по нему разные участки стора. Если взглянуть на то, как это выглядит на уровне combineReducers, то в этом нет ничего страшного:

            // псевдо код для пояснения:
            
            combineReducers({
              storeSection1: reducer1,
              storeSection2: reducer2
            });
            
            // классический вариант:
            
            // reducer1
            // здесь state - это storeSection1
            function reducer1(state, {type, data}){ 
              switch(type){
                case SOMETHING_GLOBAL_HAPPENS: return {...state, newValue: data.newValue};
                case SOMETHING_FOR_REDUCER1: return {...state, newValue: data.newValue};
              }
            }
            
            // reducer2
            // здесь state - это storeSection2
            function reducer1(state, {type, data}){  
              switch(type){
                case SOMETHING_GLOBAL_HAPPENS: return {...state, newValue: data.newValue};
                case SOMETHING_FOR_REDUCER2: return {...state, newValue: data.newValue};
              }
            }
            
            // В моём варианте в точности то же самое, но в другом немного codestyle:
            
            // reducer1
            reducer = {
                SOMETHING_GLOBAL_HAPPENS: (state, {newValue}) => { ...state, newValue },
                SOMETHING_FOR_REDUCER1: (state, {newValue}) => { ...state, newValue }
            }
            
            // reducer2
            reducer = {
                SOMETHING_GLOBAL_HAPPENS: (state, {newValue}) => { ...state, newValue },
                SOMETHING_FOR_REDUCER2: (state, {newValue}) => { ...state, newValue }
            }
            
            • 0
              Это будет работать нормально ровно до тех ор, пока вы контролируете все эти редьюсеры и их логику. А когда используется сторонняя библиотека, то могу быть болшие проблемы. Развязывание экшн типов и констант им соотвествующих — это благо.
              // your reducer
              const initialState = { num: 1 }
              
              TYPE1: ( { num } ) => ({ num: num + 1 })
              
              // third-party reducer which source code you can not affect
              TYPE1: (state) => throw new Error('Blow up the world') 
              

              Еще веселее станет дебажить, если над проектом работает несколько человек, и вы назвали свои экшн типы одниаково
              • 0
                Давайте ещё раз проясним: разные редьюсеры аффектят разные разделы стора — так устроен combineReducers
                Один редьюсер может использовать redux-refine и тогда модули с экшенами должны импортировать типы экшенов из этого редьюсера, при этом одновременно другой редьюсер может использовать классический стиль и тогда Вам необходимо будет импортировать константы, там, где Вы диспатчите эти экшены
                Одно другому не мешает, а throw Error() из редьюсера свалит апликуху в любом случае, так что не мутите воду, а поймите суть
          • 0
            Попробуй Mobx, там такого беспредела нет.
            • +1
              Поддерживаю и даже скажу больше, baobab — тоже вещь! Но корпорейт неумолим и во многих проектах всё таки redux
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Твой ник тебе в помощь. Или throttle.
                  Каждый MOUSE_MOVE будет складировать в очередь свое действие, к примеру, точки движения или цвета.
                  И только каждый N-й MOUSE_MOVE будет вызывать полное действие, типа записи в пользовательскую историю движений, как это примерно выглядит в photoshop.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +1
                      Твой ник тебе в помощь. Или throttle

                      Как вы себе представляете debounce в рисовалке? Вот я провел мышкой, нарисовал какую-нибудь линию кривую. И за счет дебаунса вместо кривой она станет прямой — вместо всех точек mousemove будет только первая и последняя.

                      В Фотошоп записывается каждое движение мыши, естественно, а не так, как вы говорите. Просто при mouseup все эти движения отмечаются как единое действие.
                    • 0

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


                      Просто следуйте простым правилам: если это простой переносимый компонент, не реализующий бизнес логику конкретного приложения, то он подучает данные через props и возвращает их в аргументах обработчиков событий, при этом не брезгуя хранением каких либо значений, использующихся только для собственных нужд, в this.state


                      Ярким примером такого поведения могут служить controlled components

                      • НЛО прилетело и опубликовало эту надпись здесь
                        • 0
                          Так у Вас же не постоянно движение мыши мониторятся, а в момент выполнения какого то действия, так? на пример при перемещении ноды, или ребра.
                          У Вас основная нагрузка приходится не на обновление стора, а на перерисовку UI при его обновлении, так как на пример при перемещении узла, со стороны стора у Вас меняется только объект с данными соответствующего узла, ссылка на массив узлов обновляется, так как в редюсере формируется новый массив (что да, при большом количестве узлов может быть немного тяжеловатым, но это надо ооочень много элементов, так как все, кроме изменяемого — просто копирование ссылок на объекты)
                          И меняется сам стор (NextStore !== Store), но те ветки стора, которых изменения не коснулись просто переносятся в новое состояние как есть.
                          Далее летит update компонентам и вот тут надо всё разрулить правильно, либо контролируя кто должен перерисоваться, при помощи shouldComponentUpdate, либо используя reselect, для тех же целей (подробнее тут) Но поверьте, самому стору такой поток событий не особо в тягость, особенно, если принять во внимание совет webdevium и всандалить на этом потоке лёгкий троттлинг на малых таймингах
                      • 0

                        А где именно вы формируете action-"логи"? Если руками, то поддерживайте склейку определённых action-ов (например по маске для action.type). Если в redux-devtools — то так ли сильно оно вам надо?
                        А вообще я обычно все подобные mouseHover вещи реализовываю через setState или ещё более обходными путями. Не в последнюю очередь из-за того, что react-redux будет вызывать все свои callback-и для connect-утых компонентов на каждый тик вашего mouseHover. Это, в случае большого приложения, легко может привести к тормозам.
                        Вообще говоря, чем дальше в лес, тем больше кода я пишу в обход redux, оставляя там только то, что имеет прямое отношение к бизнес-логике (99% кода), а не к сию-миллисекундному интерактиву, анимациям (не всё удаётся сделать через css).

                      • 0

                        Погуглите ducks-pattern + redux-act. Никаких констант. Экшены с редюсерами живут в одном файле. И тестить это проще. Я опубликовал заметку недавно.

                        • 0
                          Экшены с редюсерами живут в одном файле

                          ну ок, даже так, сравните это с этим
                          код чище, консистентность типов действий согласована на ключах хэша редьюсеров
                          Это применимо даже к duck-patterns, потому что не является ими — идея не в этом.
                          • 0
                            а что касается redux-act, то там тоже всё наоборот. Разъясню на пальцах — то, что отправляет события определяет их имена и как бы рассказывает стору: «я что то там диспатчу, значит ты должен быть на это подписан», создавая зависимость, направленную в сторону, обратную data flow
                            Правильней так: «Стор подписан на определённые типы действий, отправка которых должна быть имплементирована в экшенах»
                            В итоге мы получаем разработку, в которой мы работаем с состоянием приложения, не зависимо от того, что его меняет, так как инициация изменений может происходить в куче разных мест, а прилетают они в единое, четко определённое место и я могу поспорить на счет удобства тестирования — с redux-refine всё тестируется на много лучше и тесты могут быть сколь угодно атомарными
                            • 0

                              Посмотрите первый пример кода в заметке. Я работаю в одном месте с объявлением экшена и реализацией редюсера. Что особенно полезно при поддержке кода — один клик мышкой вместо трех.

                          • 0

                            Я пришёл к следующей схеме:


                            Файл рядового модуля:
                            const PRE = 'уникальный_префикс';
                            const OPEN = `${PRE}_OPEN`;
                            export const open = id => ({ type: OPEN, id });
                            
                            export const map =
                            {
                              [OPEN]: (st, { id }) => ({ ...st, id, active: true }),
                            };

                            Файл вышестоящего reducer-а:
                            import { map } from './my-module';
                            
                            export const reducer = (st, action) =>
                            {
                              if(/^somePrefix/.test(action.type))
                                return map[action.type](st, action);
                              // другие map-reducer-ы
                            };

                            В упрощённом виде как-то так. На деле же несколько сложнее, т.к. префиксы у action-ов могут быть многосоставными, если они глубоко по иерархии. Случайно поломать что-то опечатавшись не получится, т.к. оно либо сломается совсем, либо будет работать. Каждому action-у 1 и ровно 1 handler.


                            Отказываться от дубляжа aciton-type-ов не стал, т.к. иначе мне пришлось бы дублировать их в качестве ключей map-ов, и есть вероятность допустить опечатку.


                            Держу actionCreator-ы, action type-ы, selector-ы и их map-reducer-ы в одном файле у модулей, т.к. это оказалось наиболее удобной схемой для endpoint-модулей. Внешняя часть приложения (по сути его каркас) сделана по старинке (когда всё разбросано по разным файлам), т.к. представляет из себя как бы один внешний модуль. И меня часто терзает мысль, что сделай я всё компактнее и поближе, было бы удобнее.


                            Ну и по необходимости можно менять сигнатуру для этих map-reducer-ов. К примеру передавать внутрь не только state для обновления и action, но и какую-либо внешнюю иную часть state-а, данные которой нужны для вычисления, но сами не изменяются (этим action-ом).

                            • 0
                              Я чтото не понял, или в статье переизобрели enum и struct?
                              • 0
                                Нет, не переизобрели, а просто начали использовать там где давно пора было )))
                              • +1
                                Redux был построен на идее Free Monad. Если перевести на ООП терминологию то это паттерн Interpretator. Actions — это DSL, а reducers — это Интерпретатор. Основной смысли и преимущество в том, что уровень Actions(DSL) изолирован от того, как он должен интерпретироваться и в любой момент можно для него написать другой интерпретатор (более эффективный, либо с измененной логикой).
                                Вы же в Actions делаете связь с reducer, т.е. ваша абстракция начинает зависеть от реализации.
                                Я не говорю, что ваше решение плохое, но это уже не redux. Возникает тогда логичный вопрос, а зачем вам вообще тогда Actions?
                                Что такое store — это контейнер состояния. Основываясь на том как вы его используете, могу предложить вам перейти просто на классы.
                                class Store {
                                    action1() {
                                    }
                                    action2() {
                                    }
                                }
                                

                                • 0

                                  как то вы усложняете ) Всё гораздо проще. Вот что я сделал:
                                  Заменил конструкцию switch-case на выбор по ключу в хэшэ
                                  пояснение:


                                  function choice(selector){
                                    switch(selector) {
                                      case 'one': return 1;
                                      case 'two': return 2;
                                      case 'three': return 3;
                                      default: return 4;
                                    }
                                  }

                                  заменил на


                                  function choice(selector) {
                                    return ({
                                      one: 1,
                                      two: 2,
                                      three: 3
                                    })[selector] || 4;
                                  }

                                  Затем вынес хэш из скопа функции, что бы не пересоздавать его при каждом вызове, а каждое свойство этого хэша сделал методом, которому передаются необходимые данные в аргументах.
                                  Затем просто вместо объявления констант заюзал экспорт ключей хэша, так как они по определению уникальны, создав таким образом возможность импортировать эти ключи в экшенах из редьюсеров, что позволяет наглядно показать, с каким редьюсером работает данный модуль с экшенами.
                                  И не надо городить огород, так как redux-refine работает в точности так же, как и классический редьюсер — именно один в один, почему я и сказал, что можно его юзать одновременно с обычным подходом.
                                  Это не какая то новая архитектура, не какой то там новый наворот — это просто улучшение кодестайла и наглядности связей между модулями классического редакса.

                                  • 0

                                    Пожалуй это разъяснение тоже не плохо бы добавить в статью :)

                                    • 0

                                      А покажите ваши actionCreator-ы. Всё никак не могу их найти.

                                      • 0
                                        Не поверите, там же где и Ваши ))) Они то тут при чем? )
                                        • 0

                                          А я не совсем понимаю откуда они возьмут type для своих action-ов. Или просто перепечатываете? Едва ли там () => ({ type: Object.keys(map)[3], ... }).

                                          • 0
                                            import { 
                                              actionTypes as authActionsTypes 
                                            } from `${APP_DIR}/reducers/auth.js`;
                                            
                                            const {
                                              AUTH_LOGIN,
                                              AUTH_REGISTER,
                                              AUTH_SUCCESS,
                                              AUTH_REJECTED,
                                              AUTH_LOGOUT,
                                              ROLE_UPDATE,
                                              PROFILE_UPDATE,
                                            } from authActionsTypes;
                                            
                                            // и всё, что Вашей душе угодно - у Вас есть все константы
                                            
                                            

                                            Непонимание взаимно — Вы о чём?
                                            • 0
                                              упс… не успел исправить…
                                              вот:
                                              import { 
                                                actionTypes as authActionsTypes 
                                              } from `${APP_DIR}/reducers/auth.js`;
                                              
                                              const {
                                                AUTH_LOGIN,
                                                AUTH_REGISTER,
                                                AUTH_SUCCESS,
                                                AUTH_REJECTED,
                                                AUTH_LOGOUT,
                                                ROLE_UPDATE,
                                                PROFILE_UPDATE,
                                              } = authActionsTypes;
                                              
                                              // и всё, что Вашей душе угодно - у Вас есть все константы
                                              
                                              
                                              • 0

                                                Теперь ясно. Т.е. по сути от бойлерплейта вы не избавились. У вас первоисточник типов лежит в map, а у меня по старинке в переменных, а в map задаётся через [LOGIN]: (state, action) =>.

                                                • 0
                                                  о, кстати да, можно было и map заюзать, но в принципе хэш тоже пойдёт
                                                  и нет, не `[LOGIN]:`, a просто `LOGIN:` — ключи хеша сами по себе и есть объявление констант
                                                  • 0

                                                    Пардон, под map я имел ввиду как раз hash-map, т.е. обычный объект. А разница между моим [LOGIN] и LOGIN большая, первый это переменная с префиксами (например OPEN = 'USER_DLG_OPEN'). Если экшн-типы определяются в том же файле, что и reducer в этом проблемы нет. Я тут чуть подробнее описал.

                                                    • 0
                                                      Ключи в хешах редьюсеров и есть экшнтайпы — они более нигде не объявляются ) Нет констант вообще )
                                            • –1
                                              Вы можете производить любую декомпозицию редьюсеров по экшенам, следуя схемам бизнес логики, всё то, что обычно запутывает код в спагетти, сконцентрировано в облаке экшенов — это как нанопроцессы, которые рождаются, что нибудь делают и умирают — вся динамика приложения собрана в экшенах, на которые реагирует единое, удобно каталогизированное состояние, отраженное в интерфейсе, позволяющем эмитить экшены, а так же в слое автоматизации, также умеющем эмитить экшены и так далее — круг замкнут, всё кристально понятно — твори себе в удовольствие :)
                                    • 0
                                      Покритикуете такой подход?
                                      // actions.js
                                      import { createAction } from 'redux-actions'
                                      export const someAction = createAction('SOME_ACTION')
                                      
                                      // reducers.js
                                      import { handleAction } from 'redux-actions'
                                      import { Map } from 'immutable'
                                      import * as actions from './actions'
                                      
                                      const someAction = (state, action) => {
                                        const { value } = action.payload
                                        return state.set('value', value)
                                      }
                                      
                                      export default [
                                        handleAction(actions.someAction().type, someAction, Map()),
                                        // ... other reducers
                                      ]
                                      
                                      // rootReducer.js
                                      import reducers from './reducers'
                                      const rootReducer = (state, action) => reducers.reduce((state, reducer) => reducer(state, action), state)
                                      

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