
¿De qué se tratará?
Veamos las metamorfosis de los reductores en mis aplicaciones Redux / NGRX en los últimos años. Comenzando con el switch-case
roble, continuando con la selección del objeto por clave y terminando con clases con decoradores, blackjack y TypeScript. Intentaremos revisar no solo la historia de este camino, sino también encontrar alguna relación causal.
Si usted y yo hacemos preguntas sobre la eliminación de una repetitiva en Redux / NGRX, este artículo puede ser interesante para usted.
Si ya utiliza el enfoque para seleccionar un reductor de un objeto por clave y está harto de él, puede pasar inmediatamente a "Reductores basados en la clase".
Caja de interruptor de chocolate
Por lo general, la switch-case
del switch-case
vainilla, pero me pareció que esto discriminaba seriamente a todos los demás tipos de switch-case
del switch-case
.
Entonces, echemos un vistazo a un problema típico de la creación asincrónica de una entidad, por ejemplo, un Jedi.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false,
Seré muy franco y admitiré que nunca he usado switch-case
en mi práctica. Me gustaría creer que incluso tengo una lista de razones para esto:
switch-case
demasiado fácil de romper: puede olvidarse de insertar break
, puede olvidarse del default
.switch-case
demasiado detallado.switch-case
casi O (n). Esto no es muy importante en sí mismo, porque Redux no se jacta de un rendimiento impresionante en sí mismo, pero este hecho enfurece a mi conocedora interna de la belleza.
La forma lógica de combinar todo esto la ofrece la documentación oficial de Redux : elegir un reductor del objeto por clave.
Seleccionar un reductor de un objeto por clave
La idea es simple: cada cambio de estado puede describirse mediante una función de estado y acción, y cada función tiene una determinada clave (campo de type
en la acción) que le corresponde. Porque type
es una cadena, nada nos impide descubrir un objeto para todas esas funciones, donde la clave es type
y el valor es una función de conversión de estado puro (reductor). En este caso, podemos elegir el reductor necesario mediante la tecla (O (1)), cuando llega una nueva acción al reductor raíz.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJediMap = { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), } const reducerJedi = (state = reducerJediInitialState, action) => {
Lo más delicioso es que la lógica dentro de reducerJedi
sigue siendo la misma para cualquier reductor, y podemos reutilizarlo. Incluso hay una nano-biblioteca redux-create-reducer para esto .
import { createReducer } from 'redux-create-reducer' const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJedi = createReducer(reducerJediInitialState, { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), })
Parece que no pasó nada. Es cierto que una cucharada de miel no está exenta de un barril de alquitrán:
- Para reductores complejos, tenemos que dejar comentarios, porque Este método no proporciona una salida de la caja para proporcionar alguna metainformación explicativa.
- Los objetos con un montón de reductores y llaves no se leen bien.
- Cada reductor tiene solo una clave. Pero, ¿qué pasa si quieres ejecutar el mismo reductor para varios juegos de acción?
Casi estallé en lágrimas de felicidad cuando me mudé a reductores basados en clases, y a continuación explicaré por qué.
Reductores Basados en Clase
Bollos:
- Los métodos de clase son nuestros reductores, y los métodos tienen nombres. Solo la información meta que dice lo que hace este reductor.
- Los métodos de clase se pueden decorar, que es una forma simple y declarativa de vincular los reductores y sus acciones correspondientes (es decir, acciones, ¡no solo una acción!)
- Debajo del capó, puede usar los mismos objetos para obtener O (1).
Al final, me gustaría obtener algo así.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi {
Veo la meta, no veo obstáculos.
Paso 1. Decorador @Action
.
Necesitamos que en este decorador podamos pegar cualquier cantidad de acciones, y que estas ZhKshny se guarden como algún tipo de metainformación, a la que luego se puede acceder. Para hacer esto, podemos usar los maravillosos metadatos reflectivos polyfill, que parchean Reflect .
const METADATA_KEY_ACTION = 'reducer-class-action-metadata' export const Action = (...actionTypes) => (target, propertyKey, descriptor) => { Reflect.defineMetadata(METADATA_KEY_ACTION, actionTypes, target, propertyKey) }
Paso 2. Convierta la clase en, de hecho, un reductor.
¡Dibuja un círculo, dibuja un segundo, y ahora un poco de magia y consigue un búho!
Como sabemos, cada reductor es una función pura que toma el estado y la acción actuales y devuelve un nuevo estado. Una clase es, por supuesto, una función, pero no exactamente la que necesitamos, y las clases ES6 no se pueden invocar sin new
. En general, necesitamos transformarlo de alguna manera.
Por lo tanto, necesitamos una función que tome la clase actual, revise cada uno de sus métodos, recopile metainformación con tipos de acciones, recopile un objeto con reductores y cree el reductor final a partir de este objeto.
Comencemos recolectando meta información.
const getReducerClassMethodsWthActionTypes = (instance) => {
Ahora podemos convertir la colección resultante en un objeto
const getReducerMap = (methodsWithActionTypes) => methodsWithActionTypes.reduce((reducerMap, { method, actionType }) => { reducerMap[actionType] = method return reducerMap }, {})
Entonces la función final podría verse así:
import { createReducer } from 'redux-create-reducer' const createClassReducer = (ReducerClass) => { const reducerClass = new ReducerClass() const methodsWithActionTypes = getReducerClassMethodsWthActionTypes( reducerClass, ) const reducerMap = getReducerMap(methodsWithActionTypes) const initialState = reducerClass.initialState const reducer = createReducer(initialState, reducerMap) return reducer }
A continuación, podemos aplicarlo a nuestra clase ReducerJedi
.
const reducerJedi = createClassReducer(ReducerJedi)
Paso 3. Observamos lo que sucedió como resultado.
¿Cómo vivir?
Algo que dejamos detrás de escena:
- ¿Qué pasa si el mismo tipo de acción corresponde a reductores múltiples?
- Sería genial agregar immer fuera de la caja.
- ¿Qué pasa si queremos usar clases para crear nuestras acciones? ¿O funciones (creadores de acción)? Me gustaría que el decorador pueda aceptar no solo tipos de acciones, sino también creadores de acciones.
Una pequeña biblioteca de clase reductora tiene toda esta funcionalidad con ejemplos adicionales.
Vale la pena señalar que la idea de usar clases para reductores no es nueva. @amcdnl una vez creó una gran biblioteca de ngrx-actions , pero parece que ahora lo ha calificado y cambió a NGXS . Además, quería un tipeo más riguroso y restablecer el balasto en forma específica para la funcionalidad angular. Aquí hay una lista de las diferencias clave entre reducer-class y ngrx-actions.
Si te gustó la idea de clases para reductores, entonces también te gustaría usar clases para tus acciones. Echa un vistazo a flux-action-class .
Espero que no hayas perdido el tiempo en vano, y el artículo fue al menos un poco útil para ti. Por favor patea y critica. Aprenderemos a codificar mejor juntos.