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.
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:
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/