📜 ⬆️ ⬇️

Organizing reducer through standard class

Greetings, today I am going to talk to you about how Reducer is organized. And to tell where I started and what I came to.


So, there is a certain standard on the organization of the Reducer and it looks like this:


export default function someReducer(state = initialState, action) { switch (action.type) { case 'SOME_REDUCER_LABEL': return action.data || {}; default: return state; } } 

Everything is simple and clear, but having worked with such constructions a little, I realized that this method has a number of difficulties.



Now we had to let the saga know what kind of reducer needed to be called after running the side effect.


The most sensible option I have found is to make the action creator.


And our previous code began to look like this:


  import { FetchSaga } from '../../helpers/sagasHelpers'; const SOME_REDUCER_LABEL = 'SOME_REDUCER_LABEL'; export const someReducerLabelActionCreator = FetchSaga.bind(this, SOME_REDUCER_LABEL); export default function someReducer(state = initialState, action) { switch (action.type) { case SOME_REDUCER_LABEL: return action.data || {}; default: return state; } } 

FetchSaga is the action generator function (hereafter action creator) for the saga, which requests data from the server and dispatches it to the reducer, the label of which was transferred to the function at the initialization stage (SOME_REDUCER_LABEL).


Now, the reducer labels were either exported from the reducer, or the action creator was exported from the reducer for both the saga and the standard saga. And such a handler was created for each tag. This only added a headache, because once I opened the reducer, I counted 10 constants defining labels, then several calls for different action creators for the sagas and then another function for processing the state of the reducer, it looked like this


 import { FetchSaga } from '../../helpers/sagasHelpers'; const SOME_REDUCER_LABEL1 = 'SOME_REDUCER_LABEL1'; итд .... const SOME_REDUCER_LABEL10 = 'SOME_REDUCER_LABEL10'; export const someReducerLabelActionCreator1 = FetchSaga.bind(this, SOME_REDUCER_LABEL1); и тд ..... export const someReducerLabelActionCreator10 = FetchSaga.bind(this, SOME_REDUCER_LABEL10); export default function someReducer(state = initialState, action) { switch (action.type) { case SOME_REDUCER_LABEL: return action.data || {}; case SOME_REDUCER_LABEL1: return action.data || {}; case SOME_REDUCER_LABEL2: return action.data || {}; case SOME_REDUCER_LABEL3: return action.data || {}; .... default: return state; } } 

When importing all these actions into the controller, that one was also so bloated. And it hurt.


After reviewing a few reducer'ov, I figured that we write a lot of service code, which never changes. Plus, we must ensure that we send the cloned state to the component.


Then I had the idea to standardize the reducer. The tasks before him were not complicated.


  1. Check incoming action and return the old state if action is not for the current reducer or automatically clone the state and return to the handler method, which will change state and return to the component.
  2. You should stop using labels, instead the controller should receive an object containing all the action creators for the reducer of interest to us.
    Thus, by importing such a set once, I can pass through it any number of action creators for the dispatch functions from the reducer to the controller without having to re-import
  3. instead of using a clumsy switch-case with a common namespace, on which the linter is obscene, I want to have a separate method, for each action to which the cloned state of the reducer and the action itself will be passed
  4. It would be nice to be able to inherit a new reducer from a reducer. In case of repetition of logic, but for example for a different set of labels.

The idea seemed viable to me and I decided to try to implement it.


This is how the average reducer now looks


  // это наш стандартизированный класс, потомок которого будет управлять состоянием в данном reducer'е import stdReducerClass from '../../../helpers/reducer_helpers/stdReducer'; class SomeReducer extends stdReducerClass { constructor() { super(); /** Уникальный идентифактор reducer'а. По которому reducer будет узначать свои actionы, которые он же породил */ this.prefix = 'SOME_REDUCER__'; } /** декларация набора методов, которыми может оперировать данный reducer - type - тип, он выполняет двойную функцию. Во-первых при соединении с префиксом мы получим конечную метку, которая будет передана в action creator, например SOME_REDUCE__FETCH. Так же type являться ключом по которому можно отыскать нужный action creator в someReduceInstActions - method - Метод, который примет измененное состояние и action, выполнить какие то действия над ним и вернет состояние в компонент - sagas - это не обязательный параметр, который указывает классу, какой тип сайд эффекта следует выполнить сначала. В случае представленном ниже, будет создан action creator для саги, куда будет автоматически добавлена метка SOME_REDUCE__FETCH, После того, как сага отработает, она отправит полученные данные в reducer используя переданную ранее метку. */ config = () => [ { type: 'fetch', method: this.fetch, saga: 'fetch' }, { type: 'update', method: this.update }, ]; // получаем конфигурацию методов и генерируем на их основе нужные нам action creators init = () => this.subscribeReduceOnActions(this.config()); // реализация обработчика, которые примет данные от саги fetch = (clone, action) => { // какие то действия над клонированным состоянием return clone; }; // реализация обработчика, которые просто что то сделает с клонированным состоянием update = (clone, action) => { // какие то действия над клонированным состоянием return clone; }; } const someReducerInst = new SomeReducer(); someReducerInst.init(); // генерируем список action creators на основе config // получаем список созданных action creator для дальнейшего использования в контроллерах export const someReducerInstActions = someReducerInst.getActionCreators(); // вешаем проверку на состояния. Каждый раз checkActionForState будет проверять входящий Action и определять, относится ли он к данному reducer'у или нет export default someReducerInst.checkActionForState; 

stdReducerClass from the inside looks like this


 import { cloneDeep } from 'lodash'; //для клонирования используется зависимость lodash // так же я импортирую саги непосредственно в родителя, так как они типовые и нет смысла переопределять их каждый раз import { FetchSaga } from '../helpers/sagasHelpers/actions'; export default class StdReducer { _actions = {}; actionCreators = {}; /** UNIQUE PREFIX BLOCK START */ /** префикс мы храним в нижнем регистре, для единообразия. Как уже говорилось, это важный элемент, если него не указывать, то reducer не распознает свои actionы или все они будут ему родными */ uniquePrefix = ''; set prefix(value) { const lowedValue = value ? value.toLowerCase() : ''; this.uniquePrefix = lowedValue; } get prefix() { return this.uniquePrefix; } /** INITIAL STATE BLOCK START */ /** используя сеттер initialState можно указать начальное состояние для reducer'а. */ initialStateValues = {}; set initialState(value) { this.initialStateValues = value; } get initialState() { return this.initialStateValues; } /** PUBLIC BLOCK START */ /** * Тот самый метод который вызывается при в init() потомка. Данный метод создает, для каждой записи в массиве Config, action creator используя метод _subscribeAction * actionsConfig - список настроек определенных в потомке, где каждая запись содержит {type, method, saga?} если не указан параметр сага, то будет создан стандартный action creator который будет ожидать на вход объект с произвольными свойствами */ subscribeReducerOnActions = actionsConfig => actionsConfig.forEach(this._subscribeAction); /** Для каждой настройки вызывается метод _subscribeAction, который создает два набора, где ключом является имя метки переданное в type. Таким образом, reducer будет определять, какой метод является обработчиком для текущего actionа. */ _subscribeAction = (action) => { const type = action.type.toLowerCase(); this._actions[type] = action.method; // добавляем метод в набор обработчиков состояний this.actionCreators[type] = this._subscribeActionCreator(type, action.saga); // добавляем новый action creator в набор } /** _subscribeActionCreator - данный метод определяет, action creator какого типа должен быть создан на основе полученной конфигурации - если параметр saga не указан в конфигурации, то будет создан по умолчанию - если указан fetch то будет вызвана сага для отправки и получения данных по сети, а результат вернется в обработчик по переданной метке Метод соединяет переданный ему type из конфига с префиксом, и получает метку, которую передает в action creator, то есть, если префикс имел вид SOME_Reducer__, а тип в конфиге содержал FETCH, то в результате мы получим SOME_Reducer__FETCH, это и отправиться в action creator */ _subscribeActionCreator = (type, creatorType) => { const label = (this.prefix + type).toUpperCase(); switch (creatorType) { case 'fetch': return this._getFetchSaga(label); default: return this._getActionCreator(label); } } /** _getFetchSaga - привязывает нашу метку к саге, чтобы она понимала по какому адресу отправлять конечные данные */ _getFetchSaga = label => FetchSaga.bind(this, label); /** _getActionCreator - стандартный action creator, с уже зашитой в него меткой, все что нужно, это передать полезную нагрузку. */ _getActionCreator = label => (params = {}) => ({ type: label, ...params }); /** Это самая главная функция, которая принимает входящее состояние и playload. Она же распознает свои actionы и клонирует состояние, для дальнейшей обработки */ checkActionForState = (state = this.initialState || {}, action) => { if (!action.type) return state; const type = action.type.toLowerCase(); const prefix = this.prefix; Из входящего типа мы пытаемся удалить префикс, чтобы получить имя метода, который надо вызвать. const internalType = type.replace(prefix, ''); // по полученному ключу ищем соответствие в обработчиках if (this._actions[internalType]) { // Если такой обработчик есть - создаем клон состояния const clone = cloneDeep(state); // запускаем обработчик, передаем ему клонированное состояние, входящий action как есть, а результат выбрасываем наружу // так как мы обязаны что то вернуть return this._actions[internalType](clone, action); } // если обработчика нет, то этот action не для нас. Можно вернуть старое состояние return state; } /** Это просто геттер для получения всех action creator, которые доступны для reducer */ getActionCreators = () => this.actionCreators; } 

How does it look in the controller? That's how


 import { someReducerInstActions } from '../../../SomeReducer.js' const mapDispatchToProps = dispatch => ({ doSoAction: (params) => dispatch(someReducerInstActions.fetch(url, params)), doSoAction1: (value, block) => dispatch(someReducerInstActions.update({value, block})), }); 

So, what we have in the end:


  1. got rid of a bunch of tags
  2. got rid of heaps of imports in the controller
  3. removed the switch-case
  4. nailed the saga once and now we can expand their set in one place, being sure that all the heirs will automatically receive additional side effects handlers
  5. Got the opportunity to inherit from reducer'ov, if there is an adjacent logic (at the moment it did not come in handy to me =))
  6. Shifted responsibility for cloning from the developer to the class, who will not forget to do it.
  7. there is less routine when creating a reducer
  8. Each method has an isolated namespace.

I tried to describe everything as detailed as possible =) Sorry, if confused, the Chukchi is not a writer. I hope that my experience will be useful to someone.


The actual example can be found here.


Thank you for reading!


UPD: corrected errors. He wrote at night, poorly read. Thank you so delicately pointed at them =)



Source: https://habr.com/ru/post/439730/