Saludos, hoy voy a hablar contigo sobre cómo está organizado Reducer. Y para decir dónde comencé y a qué llegué.
Entonces, hay un cierto estándar para organizar Reducer y se ve así:
export default function someReducer(state = initialState, action) { switch (action.type) { case 'SOME_REDUCER_LABEL': return action.data || {}; default: return state; } }
Aquí todo es simple y claro, pero después de haber trabajado un poco con tales construcciones, me di cuenta de que este método tiene varias dificultades.
- Las etiquetas deben almacenarse de alguna manera, porque comenzaron a arrastrarse a lo largo del proyecto y se arrastraron mucho más allá de los controladores.
- Las etiquetas tenían que hacerse únicas, porque de lo contrario podría haber una intersección con otros reductores
- La mayor parte del tiempo al trabajar con una estructura de este tipo se dedicaba a organizar el código en lugar de procesar los datos entrantes.
- Y cuando hay muchas etiquetas en el reductor, el código se vuelve descuidado y difícil de leer, y el espacio de nombres general no me agradó abiertamente.
Casi al mismo tiempo, comenzamos a usar sagas para procesar los efectos secundarios. Esto nos permitió facilitar en gran medida la comunicación con el lado del servidor sin el uso de devoluciones de llamada.
Ahora teníamos que hacerle saber a la saga qué reductor debía llamarse después de que el efecto secundario funcionara.
La opción más sensata que he encontrado es crear un creador de acciones.
Y nuestro código anterior comenzó a verse así:
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 es una función generadora de acciones (en lo sucesivo, creador de acciones) para una saga que solicita datos de un servidor y los envía a un reductor, cuya etiqueta se pasó a la función en la etapa de inicialización (SOME_REDUCER_LABEL).
Ahora, las etiquetas del reductor se exportaron desde el reductor o el creador de la acción se exportó desde el reductor tanto para la saga como para la estándar. Además, se creó un controlador de este tipo para cada etiqueta. Esto solo agregó un dolor de cabeza, porque una vez que abrí el reductor, conté 10 constantes de las etiquetas definitorias, luego algunas llamadas para varios creadores de acciones para sagas y luego la función de procesamiento de estado del reductor, se parecía a esto
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; } }
Al importar todas estas acciones al controlador, esa también se infló demasiado. Y se interpuso en el camino.
Después de ver tantos reductores, pensé que estábamos escribiendo una gran cantidad de código de utilidad que nunca cambia. Además, debemos asegurarnos de que enviamos el estado clonado al componente.
Entonces tuve la idea de estandarizar el reductor. Las tareas ante él no fueron difíciles.
- Compruebe la acción entrante y devuelva el estado anterior si la acción no es para el reductor actual o el estado de clonación automática y páselo al método del controlador, que cambiará el estado y lo devolverá al componente.
- Debe dejar de usar etiquetas, en su lugar, el controlador debe recibir un objeto que contenga todos los creadores de acciones para el reductor que nos interesa.
Por lo tanto, habiendo importado un conjunto de este tipo una vez, puedo lanzar cualquier número de creadores de acciones para enviar funciones desde el reductor al controlador sin la necesidad de volver a importar - en lugar de usar un caso de interruptor torpe con un espacio de nombres común que usa el linter, quiero tener un método separado para cada acción, en el que se transferirá el estado clonado del reductor y la acción en sí
- Sería bueno poder heredar un nuevo reductor del reductor. En caso de que la lógica se repita, pero por ejemplo para un conjunto diferente de etiquetas.
La idea me pareció viable y decidí intentar implementarla.
Así es como el reductor promedio comenzó a verse ahora
// , 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 desde adentro es como sigue
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; }
¿Cómo se ve en el controlador? Y entonces
import { someReducerInstActions } from '../../../SomeReducer.js' const mapDispatchToProps = dispatch => ({ doSoAction: (params) => dispatch(someReducerInstActions.fetch(url, params)), doSoAction1: (value, block) => dispatch(someReducerInstActions.update({value, block})), });
Entonces, ¿qué tenemos como resultado?
- se deshizo de acumular etiquetas
- se deshizo de un montón de importaciones en el controlador
- caja de interruptor eliminada
- vencieron a las sagas una vez y ahora podemos expandir su conjunto en un solo lugar, asegurándonos de que todos los herederos recibirán automáticamente controladores de efectos secundarios adicionales
- Tenemos la oportunidad de heredar de los reductores, en caso de que haya una lógica relacionada (por el momento, esto nunca me fue útil =))
- Cambiaron la responsabilidad de la clonación del desarrollador a una clase que definitivamente recordaría hacerlo.
- menos rutina al crear reductor
- Cada método tiene un espacio de nombres aislado
Traté de describir todo con el mayor detalle posible =) Lo siento, si está confundido, el Chukchi no es un escritor. Espero que mi experiencia sea útil para alguien.
→ El ejemplo actual se puede ver aquí
Gracias por leer!
UPD: errores corregidos. Escribió por la noche, lo leyó mal. Gracias por señalarlos tan delicadamente =)