Experiencia usando redux sin reductores



Me gustaría compartir mi experiencia de usar redux en una aplicación empresarial. Hablando sobre software corporativo como parte del artículo, me centro en las siguientes características:

  • En primer lugar, este es el volumen de la funcionalidad. Estos son sistemas que se han desarrollado durante muchos años, que continúan construyendo nuevos módulos o complicando lo que ya existe allí indefinidamente.
  • En segundo lugar, a menudo, si consideramos no una pantalla de presentación, sino el lugar de trabajo de alguien, entonces se puede montar una gran cantidad de componentes adjuntos en una página.
  • En tercer lugar, la complejidad de la lógica empresarial. Si queremos obtener una aplicación receptiva y agradable de usar, el cliente tendrá que hacer una parte importante de la lógica.

Los primeros dos puntos imponen restricciones en el margen de productividad. Más sobre esto más tarde. Y ahora, le propongo discutir los problemas que encuentra al usar el clásico flujo de trabajo redux, desarrollando algo más complicado que la lista TODO.

Redux clásico


Por ejemplo, considere la siguiente aplicación:

imagen

El usuario maneja una rima: obtiene una evaluación de su talento. El control con la introducción del verso se controla y se vuelve a calcular la evaluación para cada cambio. También hay un botón por el cual se restablece el texto con el resultado, y se muestra un mensaje al usuario que puede comenzar desde el principio. Código fuente en este hilo .

Organización del código:

imagen

Hay dos módulos Más precisamente, un módulo directamente es PoemScoring. Y la raíz de la aplicación con funciones comunes para todo el sistema es la aplicación. Allí tenemos información sobre el usuario, mostrando mensajes al usuario. Cada módulo tiene sus propios reductores, acciones, controles, etc. A medida que la aplicación crece, los nuevos módulos se multiplican.

Una cascada de reductores, utilizando redux-inmutable, forma el siguiente estado totalmente inmutable:

imagen

Cómo funciona

1. Despacho de control creador de acción:

import at from '../constants/actionTypes'; export function poemTextChange(text) { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: text }); }; } 

Las constantes de los tipos de acción se mueven a un archivo separado. En primer lugar, estamos muy a salvo de errores tipográficos. En segundo lugar, intellisense estará disponible para nosotros.

2. Luego se trata del reductor.

 import logic from '../logic/poem'; export default function poemScoringReducer(state = Immutable.Map(), action) { switch (action.type) { case at.POEM_TYPE: return logic.onType(state, action.payload); default: return state; } } 

El procesamiento lógico se mueve a una función de caso separada. De lo contrario, el código reductor se volverá rápidamente ilegible.

3. La lógica de procesar clics utilizando análisis léxico e inteligencia artificial:

 export default { onType(state, text) { return state .set('poemText', text) .set('score', this.calcScore(text)); }, calcScore(text) { const score = Math.floor(text.length / 10); return score > 5 ? 5 : score; } }; 

En el caso del botón "Nuevo poema", tenemos el siguiente creador de acción:

 export function newPoem() { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: '' }); dispatch({ type: appAt.SHOW_MESSAGE, payload: 'You can begin a new poem now!' }); }; } 

Primero, envíe la misma acción que restablece nuestro texto y puntaje. Luego, envíe la acción, que será capturada por otro reductor y mostrará un mensaje al usuario.
Todo es hermoso Creemos problemas para nosotros mismos:

Los problemas:


Hemos publicado nuestra solicitud. Pero nuestros usuarios, al ver que se les pidió que escribieran poesía, naturalmente comenzaron a publicar su trabajo, lo cual es incompatible con los estándares corporativos del lenguaje poético. En otras palabras, necesitamos moderar las palabras obscenas.

Lo que haremos:

  • en el texto de entrada, es necesario reemplazar todas las palabras inculturas con * censurado *
  • Además, si el usuario ha conducido una palabra sucia, debe advertirle con un mensaje de que está haciendo mal.

Bueno Solo necesitamos analizar el texto, además de calcular el puntaje, reemplazar las malas palabras. No hay problema Y también, para informar al usuario, necesita una lista de lo que hemos eliminado. El código fuente está aquí .

Rehacemos la función de lógica para que, además del nuevo estado, devuelva la información necesaria para el mensaje al usuario (palabras reemplazadas):

 export default { onType(state, text) { const { reductedText, censoredWords } = this.redactText(text); const newState = state .set('poemText', reductedText) .set('score', this.calcScore(reductedText)); return { newState, censoredWords }; }, calcScore(text) { const score = Math.floor(text.length / 10); return score > 5 ? 5 : score; }, redactText(text) { const result = { reductedText:text }; const censoredWords = []; obscenseWords.forEach((badWord) => { if (result.reductedText.indexOf(badWord) >= 0) { result.reductedText = result.reductedText.replace(badWord, '*censored*'); censoredWords.push(badWord); } }); if (censoredWords.length > 0) { result.censoredWords = censoredWords.join(' ,'); } return result; } }; 

Apliquemos ahora. Pero como? En el reductor, ya no tiene sentido llamarlo, ya que pondremos el texto y la evaluación en el estado, pero ¿qué debemos hacer con el mensaje? Para enviar un mensaje, en cualquier caso, tendremos que enviar la acción correspondiente. Entonces, estamos finalizando action-creator.

 export function poemTextChange(text) { return function (dispatch, getState) { const globalState = getState(); const scoringStateOld = globalState.get('poemScoring'); //        const { newState, censoredWords } = logic.onType(scoringStateOld, text); dispatch({ //        type: at.POEM_TYPE, payload: newState }); if (censoredWords) { //    ,    const userName = globalState.getIn(['app', 'account', 'name']); const message = `${userName}, avoid of using word ${censoredWords}, please!`; dispatch({ type: appAt.SHOW_MESSAGE, payload: message }); } }; } 

También es necesario modificar el reductor, porque ya no llama a la función lógica:

  switch (action.type) { case at.POEM_TYPE: return action.payload; default: return state; 

Que paso:

imagen

Y ahora, la pregunta es. ¿Por qué necesitamos un reductor que, en su mayor parte, simplemente devolverá la carga útil en lugar de un nuevo estado? Cuando aparezcan otras acciones que procesen la lógica en la acción, ¿será necesario registrar un nuevo tipo de acción? ¿O tal vez cree un SET_STATE común? Probablemente no, porque entonces, el inspector será un desastre. Entonces, ¿produciremos el mismo tipo de caso?

La esencia del problema es la siguiente. Si el procesamiento de la lógica implica trabajar con una pieza de estado, de la cual son responsables varios reductores, entonces debe escribir todo tipo de perversiones. Por ejemplo, los resultados intermedios de las funciones de caso, que luego deben dispersarse entre diferentes reductores mediante varias acciones.

Una situación similar, si la función de caso necesita más información que la que está en su reductor, debe llamar a la acción, donde hay acceso al estado global, seguido de enviar el nuevo estado como carga útil. Un reductor tendrá que dividirse en cualquier caso, si hay mucha lógica en el módulo. Y esto crea grandes inconvenientes.

Veamos la situación por un lado. En nuestra acción, obtenemos una parte del estado de lo global. Esto es necesario para mutarlo ( globalState.get ('poemScoring'); ). Resulta que ya sabemos en acción con qué estado está trabajando el trabajo. Tenemos un nuevo estado. Sabemos dónde ponerlo. Pero en lugar de ponerlo en uno global, lo ejecutamos con algún tipo de texto constante en toda la cascada de reductores para que pase por cada caja de interruptor y lo sustituya solo una vez. Yo de la realización de esto, arrugas. Entiendo que esto se hace para facilitar el desarrollo y reducir la conectividad. Pero en nuestro caso, ya no tiene un papel.

Ahora, enumeraré todos los puntos que no me gustan en la implementación actual, si tiene que ampliarse en profundidad por un tiempo ilimitado :

  1. Inconvenientes importantes cuando se trabaja con un estado fuera del reductor.
  2. El problema de la separación del código. Cada vez que enviamos una acción, pasa por cada reductor, pasa por cada caso. Es conveniente no molestarse cuando tiene una aplicación pequeña. Pero, si tiene un monstruo que se construyó durante varios años con docenas de reductores y cientos de casos, entonces empiezo a pensar en la viabilidad de este enfoque. Quizás, incluso con miles de casos, esto no tendrá un impacto significativo en el rendimiento. Pero, entendiendo que al imprimir texto, cada impresión causará un pasaje a través de cientos de casos, no puedo dejarlo como está. Cualquiera, el retraso más pequeño, multiplicado por el infinito, tiende al infinito. En otras palabras, si no piensa en esas cosas, tarde o temprano, surgirán problemas.

    Cuales son las opciones?

    a. Aplicaciones aisladas con sus propios proveedores . En cada módulo (sub-aplicación), deberá duplicar las partes generales del estado (cuenta, mensajes, etc.).

    b. Utilice reductores asincrónicos conectables. Esto no es recomendado por el propio Dan.

    c. Use filtros de acción en reductores. Es decir, cada envío debe ir acompañado de información sobre a qué módulo se está enviando. Y en los reductores raíz de los módulos, escriba las condiciones apropiadas. Lo he intentado No hubo tal cantidad de errores involuntarios ni antes ni después. Hay una confusión constante sobre dónde va la acción.
  3. Cada vez que se envía una acción, no solo hay una ejecución para cada reductor, sino también la recopilación del estado inverso. No importa si el estado ha cambiado en el reductor, se reemplazará en combineReducers.
  4. Cada envío obliga a mapStateToProps a procesarse para cada componente adjunto que se monta en la página. Si dividimos los reductores, tenemos que dividir los despachos. ¿Es crítico que tengamos un botón que sobrescriba el texto y muestre el mensaje con diferentes despachos? Probablemente no. Pero tengo experiencia en optimización, al reducir la cantidad de despachos de 15 a 3 permitieron aumentar significativamente la capacidad de respuesta del sistema, con la misma cantidad de lógica comercial procesada. Sé que hay bibliotecas que pueden combinar varios despachos en un lote, pero esta es una lucha con la investigación usando muletas.
  5. Al aplastar despachos, a veces es muy difícil ver lo que está sucediendo. No hay un solo lugar, todo está disperso en diferentes archivos. Es necesario buscar dónde se implementa el procesamiento buscando constantes en todos los códigos fuente.
  6. En el código anterior, los componentes y las acciones acceden directamente al estado global:

     const userName = globalState.getIn(['app', 'account', 'name']); … const text = state.getIn(['poemScoring', 'poemText']); 

    Esto no es bueno por varias razones:

    a. Los módulos deberían estar aislados idealmente. No necesitan saber en qué parte del estado viven.

    b. Mencionar los mismos caminos en diferentes lugares a menudo está lleno no solo de errores / errores tipográficos, sino que también hace que la refactorización sea extremadamente difícil en caso de cambiar la configuración del estado global o cambiar la forma en que se almacena.
  7. Cada vez más, mientras escribía una nueva acción, tuve la impresión de que estaba escribiendo código por el bien del código. Supongamos que queremos agregar una casilla de verificación a la página y reflejar su estado booleano en la historia. Si queremos una organización uniforme de acción / reductores, entonces tenemos que:

    - Registro constante de tipo acción
    - Escribe un cráter de acción
    - En el control, impórtalo y regístralo en mapDispatchToProps
    - Registrarse en PropTypes
    - Cree un handleCheckBoxClick en el control y especifíquelo en la casilla de verificación
    - Agregue un interruptor en el reductor con una llamada de función de caso
    - Escribir una función de caso en la lógica

    Por el bien de un cheque de boxeo!
  8. El estado que se genera con combineReducers es estático. No importa si ya ingresó al módulo B o no, esta pieza estará en la historia. Vacío, pero lo estará. No es conveniente utilizar el inspector cuando hay muchos nodos vacíos no utilizados en el steet.

Cómo intentamos resolver algunos de los problemas descritos anteriormente


Entonces, obtuvimos reductores estúpidos, y en los cráteres de acción / lógica escribimos pedazos de código para trabajar con estructuras inmutables profundamente incrustadas. Para deshacerme de esto, utilizo el mecanismo de selectores jerárquicos, que permiten no solo acceder al estado deseado, sino también reemplazarlo (conveniente setIn). Publiqué esto en el paquete inmutable-selectors .

Veamos nuestro ejemplo de cómo funciona ( repositorio ):
En el módulo poemScoring, describimos el objeto de selección. Describimos aquellos campos del estado al que queremos tener acceso directo de lectura / escritura. Se permite cualquier anidación y parámetros para acceder a los elementos de las colecciones. No es necesario describir todos los campos posibles en nuestro artículo.

 import extendSelectors from 'immutable-selectors'; const selectors = { poemText:{}, score:{} }; extendSelectors(selectors, [ 'poemScoring' ]); export default selectors; 

Además, el método extendSelectors convierte cada campo de nuestro objeto en una función de selector. El segundo parámetro indica la ruta a la parte del estado que controla el selector. No creamos un nuevo objeto, sino que cambiamos el actual. Esto nos da una bonificación en forma de inteligencia de trabajo:

imagen

¿Cuál es nuestro objeto? Un selector después de su expansión:

imagen

La función selectors.poemText (estado) simplemente ejecuta state.getIn (['poemScoring', 'poemText']) .

Función root (estado) : obtiene 'poemScoring'.

Cada selector tiene su propia función de reemplazo (globalState, newPart) , que a través de setIn devuelve un nuevo estado global con la parte correspondiente reemplazada.

Además, se agrega un objeto plano al que se duplican todas las teclas selectoras únicas. Es decir, si usamos un estado profundo de la forma

 selectors = { dive:{ in:{ to:{ the:{ deep:{} } } } }} 

Puede profundizar como selectors.dive.in.to.the.deep (estado) o como selectors.flat.deep (estado) .

Adelante Necesitamos actualizar la adquisición de datos en los controles:

Poema:
 function mapStateToProps(state, ownprops) { return { text:selectors.poemText(state) || '' }; } 


Puntuación:
 function mapStateToProps(state, ownprops) { const score = selectors.score(state); return { score }; } 

A continuación, cambie el reductor de raíz:

 import initialState from './initialState'; function setStateReducer(state = initialState, action) { if (action.setState) { return action.setState; } else { return state; // return combinedReducers(state, action); // } } export default setStateReducer; 

Si lo desea, podemos combinarlo con combineReducers.

Cráter de acción, por ejemplo, poemTextChange:

 export function poemTextChange(text) { return function (dispatch, getState) { dispatch({ type: 'Poem typing', setState: logic.onType(getState(), text), payload: text }); }; } 

Ya no podemos usar constantes de tipo acción, porque el tipo ahora se usa solo para visualización en el inspector. Nosotros en el proyecto escribimos descripciones de texto completo de la acción en ruso. También puede deshacerse de la carga útil, pero trato de guardarla para que en el inspector, si es necesario, entiendo con qué parámetros se llamó la acción.

Y, de hecho, la lógica misma:

  onType(gState, text) { const { reductedText, censoredWords } = this.redactText(text); const poemState = selectors.root(gState) || Immutable.Map(); //     const newPoemState = poemState //  .set('poemText', reductedText) .set('score', this.calcScore(reductedText)); let newGState = selectors.root.replace(gState, newPoemState); //    if (censoredWords) { //  ,    const userName = appSelectors.flat.userName(gState); const messageText = `${userName}, avoid of using word ${censoredWords}, please!`; newGState = message.showMessage(newGState, messageText); } return newGState; }, 

Al mismo tiempo, message.showMessage se importa desde la lógica del módulo vecino, que describe sus selectores:

  showMessage(gState, text) { return selectors.message.text.replace(gState, text); }. 

Lo que resulta:

imagen

Tenga en cuenta que tuvimos un envío, los datos en dos módulos cambiaron.
Todo esto nos permitió deshacernos de los reductores y las constantes de acción, así como resolver o sortear la mayoría de los cuellos de botella descritos anteriormente.

¿De qué otra manera se puede aplicar esto?


Este enfoque es conveniente de usar cuando es necesario asegurarse de que sus controles o módulos proporcionen trabajo con diferentes partes del estado. Digamos que un poema no es suficiente para nosotros. Queremos que el usuario pueda componer poemas en dos pestañas diferentes en diferentes disciplinas (infantil, romántica). En este caso, no podemos importar los selectores en la lógica / controles, sino especificarlos como un parámetro en el control externo:

  <Poem selectors = {selectors.hildPoem}/> <Poem selectors = {selectors.romanticPoem}/> 

Y, además, pase este parámetro a los cráteres de acción. Esto es suficiente para hacer una combinación compleja de componentes y lógica completamente cerrada, lo que facilita su reutilización.

Limitaciones cuando se usan selectores inmutables:

No funcionará usar la clave en el estado "nombre", porque para la función principal se intentará anular la propiedad reservada.

Cual es el resultado


Como resultado, se obtuvo un enfoque bastante flexible, se eliminaron las relaciones implícitas de código mediante constantes de texto, se redujo la sobrecarga mientras se mantenía la conveniencia del desarrollo. También hay un inspector redux en pleno funcionamiento con la posibilidad de viajar en el tiempo. No deseo volver a los reductores estándar.

En general, eso es todo. Gracias por su tiempo ¡Quizás alguien esté interesado en probarlo!

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


All Articles