Приветствую, сегодня я собираюсь поговорить с вами о способе организации Reduce. И рассказать с чего я начал и к чему пришел.
Итак, есть некий стандарт по организации Reduce и выглядит он следующим образом:
export default function someReduce(state = initialState, action) {
switch (action.type) {
case 'SOME_REDUCE_LABEL':
return action.data || {};
default:
return state;
}
}
Тут все просто и понятно, но немного поработав с такими конструкциями я понял что данный метод имеет ряд сложностей.
- Метки надо как то хранить, потому что они начали расползаться по проекту и уползать далеко за пределы контроллеров.
- Метки надо было делать уникальными, потому что иначе могло быть пересечение с другими редьюсами
- Большая часть времени при работе с такой структурой тратилась на организацию кода, нежели на обработку входящих данных
- И когда меток в редьюсе набирается много — код становиться неряшливым и трудно читаемым, ну и общее пространство имен меня откровенно не радовало.
Примерно в это же время, для обработки сайд эффектов мы стали применять саги. Это позволило нам значительно облегчить общение с серверной частью без использования колбеков.
Теперь нам надо было дать знать саге, какой редьюс надо было вызвать, после того, как отработает сайдэффект.
Самый разумный вариант, который я нашел, это сделать action creator.
И наш предидущий код стал выглядеть вот так:
import { FetchSaga } from '../../helpers/sagasHelpers';
const SOME_REDUCE_LABEL = 'SOME_REDUCE_LABEL';
export const someReduceLabelActionCreator = FetchSaga.bind(this, SOME_REDUCE_LABEL);
export default function someReduce(state = initialState, action) {
switch (action.type) {
case SOME_REDUCE_LABEL:
return action.data || {};
default:
return state;
}
}
FetchSaga — это функция-генератор action (далее action creator) для саги, которая запрашивает данные с сервера и диспатчит их в редьюс, метка которого была передана функции на этапе инициализации(SOME_REDUCE_LABEL).
Теперь, метки редьюсов либо экспортировались из редьюса, либо из редьюса экспортировался action creator как для саги так и типовой. Причем такой обработчик создавался на каждую метку. Это лишь добавило головной боли, потому что однажды открыв редьюсь я насчитал 10 констант определяющих метки, потом несколько вызовов для различных action creator для саг и потом еще и функцию обработки состояния редьюса, выглядело это примерно вот так
import { FetchSaga } from '../../helpers/sagasHelpers';
const SOME_REDUCE_LABEL1 = 'SOME_REDUCE_LABEL1';
const SOME_REDUCE_LABEL2 = 'SOME_REDUCE_LABEL2';
const SOME_REDUCE_LABEL3 = 'SOME_REDUCE_LABEL3';
const SOME_REDUCE_LABEL4 = 'SOME_REDUCE_LABEL4';
const SOME_REDUCE_LABEL5 = 'SOME_REDUCE_LABEL5';
....
const SOME_REDUCE_LABEL10 = 'SOME_REDUCE_LABEL10';
export const someReduceLabelActionCreator1 = FetchSaga.bind(this, SOME_REDUCE_LABEL1);
export const someReduceLabelActionCreator2 = data => ({...data, SOME_REDUCE_LABEL2});
export const someReduceLabelActionCreator3 = data => ({...data, SOME_REDUCE_LABEL3});
export const someReduceLabelActionCreator4 = data => ({...data, SOME_REDUCE_LABEL4});
export const someReduceLabelActionCreator5 = data => ({...data, SOME_REDUCE_LABEL5});
.....
export const someReduceLabelActionCreator10 = FetchSaga.bind(this, SOME_REDUCE_LABEL10);
export default function someReduce(state = initialState, action) {
switch (action.type) {
case SOME_REDUCE_LABEL: return action.data || {};
case SOME_REDUCE_LABEL1: return action.data || {};
case SOME_REDUCE_LABEL2: return action.data || {};
case SOME_REDUCE_LABEL3: return action.data || {};
....
default:
return state;
}
}
При импорте всех этих actionов в контроллер тот тоже нехило так раздувался. И это мешало.
Просмотрев так несколько редьюсов, я прикинул что мы пишем много служебного кода, который никогда не меняется. Плюс мы должны следить за тем, что отправляем в компонент клонированное состояние.
Тогда у меня родилась идея стандартизировать редьюс. Задачи перед ним стояли не сложные.
- Проверять входящий action и возвращать старое состояние, если action не для текущего редьюса или автоматически клонировать state и отдавать в метод-обработчик, который изменить состояние и отдаст в компонент.
- Следует перестать оперировать метками, вместо этого контроллер должен получать объект содержащий все action creators для интересующего нас редьюса.
Таким образом импортировав такой набор один раз, я смогу прокидовать через него любое количество action creators для dispatch функции из редьюса в контроллер без необходимости повторного импорта - вместо использование корявого switch-case с общим пространством имен, на который материться линтер, я хочу иметь отдельный метод, для каждого actionа, в который будет передано уже клонированное состояние редьюса и сам action
- неплохо бы иметь возможность наследовать от редьюса новый редьюс. На случай повторения логики, но например для другого набора меток.
Идея показалась мне жизнеспособной и я решил попробовать это реализовать.
Вот как стал выглядеть среднестатистический редьюс теперь
// это наш стандартизированный класс, потомок которого будет управлять состоянием в данном редьюсе
import stdReduceClass from '../../../helpers/reduce_helpers/stdReduce';
class SomeReduce extends stdReduceClass {
constructor() {
super();
/**
Уникальный идентифактор редьюса. По которому Редьюс будет узначать свои actionы, которые он же породил
*/
this.prefix = 'SOME_REDUCE__';
}
/**
декларация набора методов, которыми может оперировать данный редьюс
- type - тип, он выполняет двойную функцию. Во-первых при соединении с префиксом мы получим конечную метку, которая будет передана в action creator, например SOME_REDUCE__FETCH.
Так же type являться ключом по которому можно отыскать нужный action creator в someReduceInstActions
- method - Метод, который примет измененное состояние и action, выполнить какие то действия над ним и вернет состояние в компонент
- sagas - это не обязательный параметр, который указывает классу, какой тип сайд эффекта следует выполнить сначала. В случае представленном ниже, будет создан action creator для саги, куда будет автоматически добавлена метка SOME_REDUCE__FETCH,
После того, как сага отработает, она отправит полученные данные в редьюс используя переданную ранее метку.
*/
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 someReduceInst = new SomeReduce();
someReduceInst.init(); // генерируем список action creators на основе config
// получаем список созданных action creator для дальнейшего использования в контроллерах
export const someReduceInstActions = someReduceInst.getActionCreators();
// вешаем проверку на состояния. Каждый раз checkActionForState будет проверять входящий Action и определять, относится ли он к данному редьюсу или нет
export default someReduceInst.checkActionForState;
stdReduceClass изнутри выглядит следующим образом
import { cloneDeep } from 'lodash'; //для клонирования используется зависимость lodash
// так же я импортирую саги непосредственно в родителя, так как они типовые и нет смысла переопределять их каждый раз
import { FetchSaga } from '../helpers/sagasHelpers/actions';
export default class StdReduce {
_actions = {};
actionCreators = {};
/** UNIQUE PREFIX BLOCK START */
/**
префикс мы храним в нижнем регистре, для единообразия. Как уже говорилось, это важный элемент, если него не указывать,
то редьюс не распознает свои actionы или все они будут ему родными
*/
uniquePrefix = '';
set prefix(value) {
const lowedValue = value ? value.toLowerCase() : '';
this.uniquePrefix = lowedValue;
}
get prefix() {
return this.uniquePrefix;
}
/** INITIAL STATE BLOCK START */
/**
используя сеттер initialState можно указать начальное состояние для редьюса.
*/
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 который будет ожидать на вход объект с произвольными свойствами
*/
subscribeReduceOnActions = actionsConfig => actionsConfig.forEach(this._subscribeAction);
/**
Для каждой настройки вызывается метод _subscribeAction, который создает два набора, где ключом является имя метки переданное в type. Таким образом, редьюсь будет определять, какой метод является обработчиком для текущего 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_REDUCE__, а тип в конфиге содержал FETCH, то в результате мы получим SOME_REDUCE__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, которые доступны для редьюса
*/
getActionCreators = () => this.actionCreators;
}
Как же это выглядеть в контроллере? А вот так
import { someReduceInstActions } from '../../../SomeReduce.js'
const mapDispatchToProps = dispatch => ({
doSoAction: (params) => dispatch(someReduceInstActions.fetch(url, params)),
doSoAction1: (value, block) => dispatch(someReduceInstActions.update({value, block})),
});
Итак, что мы имеем в итоге:
- избавились от нагромождения меток
- избавились от кучи импортов в контроллере
- убрали switch-case
- прибили саги один раз и теперь можем расширят их набор в одном месте, будучи уверенными что все наследники автоматически получат дополнительные обработчики сайд эффектов
- Получили возможность наследовать от редьюсов, в случае если есть смежная логика( на данный момент это мне так и не пригодилось =) )
- Переложили ответственность по клонированию с разработчика на класс, который точно не забудет это сделать.
- стало меньше рутины при создании редьюса
- Каждый метод имеет изолированное пространство имен
Я старался описать все как можно подробнее =) Извините, если путано, чукча не писатель. Надеюсь что кому нибудь будет полезен мой опыт.
→ Действующий пример можно посмотреть тут
Спасибо, что дочитали!
Автор: Neffes