Otra guía de reducción de repeticiones de Redux (NGRX)


¿De qué se tratará?


Hablaremos sobre varios (cinco, para ser específicos) métodos, trucos, sacrificios sangrientos al Dios de la Empresa, que parecen ayudarnos a escribir un código más conciso y expresivo en nuestras aplicaciones Redux (¡y NGRX!). Las formas están plagadas de sudor y café. Por favor patea y critica fuertemente. Aprenderemos a codificar mejor juntos.


Honestamente, al principio solo quería contarle al mundo sobre mi nueva microbiblioteca (¡35 líneas de código!) Flux-action-class , pero mirando el número cada vez mayor de exclamaciones de que Habr pronto se convertirá en Twitter, y en su mayor parte De acuerdo con ellos, decidí intentar hacer una lectura algo más amplia. ¡Entonces, conocemos 5 formas de actualizar su aplicación Redux!


Boilerplate sale


Considere un ejemplo típico de cómo enviar una solicitud AJAX a Redux. Imaginemos que realmente necesitamos una lista de sellos del servidor.


import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = (payload) => ({ type: actionTypeCatsGetSuccess, payload, }) const actionCatsGetError = (error) => ({ type: actionTypeCatsGetError, payload: error, }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, ) 

Si no comprende por qué se necesitan fábricas para selectores aquí, puede leer sobre esto aquí.


No considero deliberadamente los efectos secundarios aquí. Este es un tema para un artículo separado lleno de ira adolescente y críticas al ecosistema existente: D


Hay varios puntos débiles en este código:


  • Las fábricas de acción son únicas por derecho propio, pero aún utilizamos tipos de acción.
  • A medida que se agregan nuevas entidades, continuamos duplicando la misma lógica para establecer el indicador de loading . Los datos que almacenamos en los data y su forma pueden variar significativamente de una solicitud a otra, pero el indicador de descarga (indicador de loading ) seguirá siendo el mismo.
  • El tiempo de ejecución del interruptor es O (n) (bueno, casi ). Esto en sí mismo no es un argumento muy fuerte, porque Redux, en principio, no se trata de rendimiento. Me enfurece más que para cada case necesites escribir un par de líneas adicionales de código de publicación, y que un switch no se puede dividir fácilmente en varios.
  • ¿Realmente necesitamos almacenar el estado de error para cada entidad por separado?
  • Los selectores son geniales. Los selectores memorizados son doblemente geniales. Nos dan una abstracción de nuestro lado, para que luego no tengamos que rehacer la mitad de la aplicación al cambiar la forma de la misma. Simplemente cambiamos el selector en sí. Lo que no es agradable a la vista es un conjunto de fábricas primitivas que solo se necesitan debido a las peculiaridades de la memorización en la reflexión .

Método 1: deshacerse de los tipos de acción


Bueno, en realidad no. Simplemente hacemos que JS los cree para nosotros.


Pensemos por un segundo sobre por qué generalmente necesitamos tipos de acción. Bueno, obviamente, para iniciar la rama lógica deseada en nuestro reductor y cambiar el estado de la aplicación en consecuencia. La verdadera pregunta es, ¿un tipo tiene que ser una cadena? Pero, ¿qué pasa si usamos clases y cambiamos por tipo?


 class CatsGetInit {} class CatsGetSuccess { constructor(responseData) { this.payload = responseData } } class CatsGetError { constructor(error) { this.payload = error this.error = true } } const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.constructor) { case CatsGetInit: return { ...state, loading: true, } case CatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case CatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } 

Todo parece ser genial, pero hay un problema: perdimos la serialización de nuestras acciones. Estos ya no son objetos simples que podemos convertir en una cadena y viceversa. Ahora confiamos en el hecho de que cada acción tiene su propio prototipo único, que, de hecho, permite que funcione un diseño como un switch en action.constructor . Sabes, me gusta mucho la idea de serializar mis acciones en una cadena y enviarlas junto con un informe de error, y no estoy listo para rechazarlo.


Por lo tanto, cada acción debe tener un campo de type ( aquí puede ver qué más debe tener cada acción respetuosa con la acción). Afortunadamente, cada clase tiene un nombre que es como una cadena. Agreguemos un type getter type cada clase que devolverá el nombre de esta clase.


 class CatsGetInit { constructor() { this.type = this.constructor.name } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.name: return { ...state, loading: true, } //... } } 

Incluso funciona, pero me gustaría pegar un prefijo para cada tipo, como sugiere el Sr. Eric en ducks-modular-redux (recomiendo mirar el tenedor de re-ducks , que es aún más genial, en cuanto a mí). Para agregar un prefijo, tendremos que dejar de usar el nombre de la clase directamente y agregar otro getter. Ahora estático


 class CatsGetInit { get static type () { return `prefix/${this.name}` } constructor () { this.type = this.constructor.type } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } //... } } 

Peinemos todo esto un poco. Reduzca el copiar y pegar al mínimo y agregue otra condición: si la acción presenta un error, entonces su payload debe ser del tipo Error .


 class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { this.type = this.constructor.type this.payload = payload this.error = payload instanceof Error } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } } 

En esta etapa, este código funciona bien con NGRX, pero Redux no es capaz de masticarlo. Jura que la acción debería ser objetos simples. Afortunadamente, JS nos permite devolver casi cualquier cosa del diseñador, pero realmente no necesitamos una cadena de prototipos después de crear la acción.


 class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { return { type: this.constructor.type, payload, error: payload instanceof Error } } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } } 

En base a las consideraciones anteriores, se escribió la micro biblioteca de clase de acción de flujo . Hay pruebas, 100% de cobertura de prueba y casi la misma clase ActionStandard con ActionStandard a genéricos para las necesidades de TypeScript. Funciona con TypeScript y JavaScript.


Método 2: No tenemos miedo de usar CombineReducers


La idea es fácil de deshonrar: use combineReducers no solo para los reductores de nivel superior, sino también para romper aún más la lógica y crear un reductor separado para la loading .


 const reducerLoading = (actionInit, actionSuccess, actionError) => ( state = false, action, ) => { switch (action.type) { case actionInit.type: return true case actionSuccess.type: return false case actionError.type: return false } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = (state = undefined, action) => { switch (action.type) { case CatsGetSuccess.type: return action.payload default: return state } } const reducerCatsError = (state = undefined, action) => { switch (action.type) { case CatsGetError.type: return action.payload default: return state } } const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, }) 

Método 3: deshacerse del interruptor


Y una vez más, una idea extremadamente simple: en lugar de switch-case use un objeto para seleccionar el campo deseado por tecla. El acceso al campo del objeto por clave es O (1), y se ve un poco más limpio en mi humilde opinión.


 const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => { //       const reducer = state[action.type] if (!reducer) { return state } //  ,    return reducer(state, action) } const reducerLoading = (actionInit, actionSuccess, actionError) => createReducer(false, { [actionInit.type]: () => true, [actionSuccess.type]: () => false, [actionError.type]: () => false, }) class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCatsError = createReducer(undefined, { [CatsGetError.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, }) 

Refactoricemos el reducerLoading . Ahora, conociendo los mapas (objetos) para reductores, podemos devolver este mapa desde reducerLoading , en lugar de devolver un reductor completo. Potencialmente, esto abre un alcance ilimitado para expandir la funcionalidad.


 const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => { //       const reducer = state[action.type] if (!reducer) { return state } //  ,    return reducer(state, action) } const reducerLoadingMap = (actionInit, actionSuccess, actionError) => ({ [actionInit.type]: () => true, [actionSuccess.type]: () => false, [actionError.type]: () => false, }) class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) /*       reducerCatsLoading: const reducerCatsLoading = createReducer( false, { ...reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ... some custom stuff } ) */ const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCatsError = createReducer(undefined, { [CatsGetError.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading), error: reducerCatsError, }) 

La documentación oficial sobre Redux también habla sobre este enfoque , sin embargo, por alguna razón desconocida, sigo viendo muchos proyectos usando switch-case . Basado en el código de la documentación oficial, el Sr. Moshe ha compilado una biblioteca para nosotros para createReducer .


Método 4: utilice el controlador de errores global


No tenemos que guardar el error para cada entidad por separado. En la mayoría de los casos, solo queremos mostrar el diálogo. El mismo diálogo con texto dinámico para todas las entidades.


Crea un controlador de errores global. En el caso más simple, podría verse así:


 class GlobalErrorInit extends ActionStandard {} class GlobalErrorClear extends ActionStandard {} const reducerError = createReducer(undefined, { [GlobalErrorInit.type]: (state, action) => action.payload, [GlobalErrorClear.type]: (state, action) => undefined, }) 

Luego, en nuestro efecto secundario, enviaremos la acción ErrorInit en el catch . Puede verse algo así cuando se usa redux-thunk :


 const catsGetAsync = async (dispatch) => { dispatch(new CatsGetInit()) try { const res = await fetch('https://cats.com/api/v1/cats') const body = await res.json() dispatch(new CatsGetSuccess(body)) } catch (error) { dispatch(new CatsGetError(error)) dispatch(new GlobalErrorInit(error)) } } 

Ahora podemos deshacernos del campo de error en nuestra tienda de gatos y usar CatsGetError solo para cambiar la bandera de loading .


 class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) }) 

Método 5: Piensa antes de memorizar


Veamos nuevamente un montón de fábricas para selectores.


Lancé makeSelectorCatsError porque ya no es necesario, como encontramos en el capítulo anterior.


 const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) 

¿Por qué necesitamos selectores memorizados aquí? ¿Qué es exactamente lo que estamos tratando de memorizar? El acceso al campo de objeto por clave, que es lo que sucede aquí, es O (1). Podemos usar funciones ordinarias no memorizadas. Use la memorización solo cuando desee cambiar los datos de la tienda antes de entregarlos al componente.


 const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading 

La memorización tiene sentido en el caso de calcular el resultado sobre la marcha. Para el siguiente ejemplo, imaginemos que cada gato es un objeto con el campo de name , y queremos obtener una cadena que contenga los nombres de todos los gatos.


 const makeSelectorCatNames = () => createSelector( (state) => state.cats.data, (cats) => cats.data.reduce((accum, { name }) => `${accum} ${name}`, ''), ) 

Conclusión


Veamos de nuevo dónde empezamos:


 import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = () => ({ type: actionTypeCatsGetSuccess }) const actionCatsGetError = () => ({ type: actionTypeCatsGetError }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, ) 

Y lo que vino a:


 class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) }) const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading 

Espero que no hayas perdido el tiempo en vano, y el artículo fue al menos un poco útil para ti. Como dije al principio, por favor patea y critica fuerte. Aprenderemos a codificar mejor juntos.

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


All Articles