Extraer la lógica del estado en un reductor
Los componentes con muchas actualizaciones de estado repartidas en muchos manejadores de eventos pueden resultar abrumadores. Para estos casos, puedes consolidar toda la lógica de actualización del estado fuera de tu componente en una única función, llamada unreductor.
Aprenderás
- Qué es una función reductora
- Cómo refactorizar
useStateauseReducer - Cuándo usar un reductor
- Cómo escribir uno correctamente
Consolidar la lógica del estado con un reductor
A medida que tus componentes crecen en complejidad, puede resultar más difícil ver de un vistazo todas las formas diferentes en las que se actualiza el estado de un componente. Por ejemplo, el componenteTaskAppa continuación mantiene un array detasksen el estado y utiliza tres manejadores de eventos diferentes para agregar, eliminar y editar tareas:
Cada uno de sus manejadores de eventos llama asetTaskspara actualizar el estado. A medida que este componente crece, también lo hace la cantidad de lógica de estado esparcida por todo él. Para reducir esta complejidad y mantener toda tu lógica en un lugar fácil de acceder, puedes mover esa lógica de estado a una única función fuera de tu componente,llamada un “reductor”.
Los reductores son una forma diferente de manejar el estado. Puedes migrar deuseState a useReduceren tres pasos:
- Pasarde establecer el estado a despachar acciones.
- Escribiruna función reductora.
- Usarel reductor desde tu componente.
Paso 1: Pasar de establecer el estado a despachar acciones
Tus manejadores de eventos actualmente especificanqué hacerestableciendo el estado:
Elimina toda la lógica de establecimiento del estado. Lo que te queda son tres manejadores de eventos:
handleAddTask(text)se llama cuando el usuario presiona “Agregar”.handleChangeTask(task)se llama cuando el usuario alterna una tarea o presiona “Guardar”.handleDeleteTask(taskId)se llama cuando el usuario presiona “Eliminar”.
Gestionar el estado con reductores es ligeramente diferente de establecer el estado directamente. En lugar de decirle a React “qué hacer” estableciendo el estado, especificas “qué acaba de hacer el usuario” despachando “acciones” desde tus manejadores de eventos. (¡La lógica de actualización del estado vivirá en otro lugar!) Así que en lugar de “establecertasks” a través de un manejador de eventos, estás despachando una acción de “agregada/cambiada/eliminada una tarea”. Esto describe mejor la intención del usuario.
El objeto que pasas adispatchse llama una “acción”:
Es un objeto JavaScript regular. Tú decides qué poner en él, pero generalmente debería contener la información mínima sobrequé sucedió. (Agregarás la funcióndispatchen un paso posterior.)
Nota
Un objeto de acción puede tener cualquier forma.
Por convención, es común darle un stringtypeque describa qué sucedió, y pasar cualquier información adicional en otros campos. Eltypees específico de un componente, así que en este ejemplo tanto'added'como'added_task'estarían bien. ¡Elige un nombre que diga qué sucedió!
Paso 2: Escribir una función reductora
Una función reductora es donde pondrás tu lógica de estado. Toma dos argumentos, el estado actual y el objeto de acción, y devuelve el siguiente estado:
React establecerá el estado con lo que devuelvas desde el reductor.
Para mover tu lógica de establecimiento de estado desde tus controladores de eventos a una función reductora en este ejemplo, harás:
- Declarar el estado actual (
tasks) como el primer argumento. - Declarar el objeto
actioncomo el segundo argumento. - Devolver elsiguienteestado desde el reductor (que React establecerá como el estado).
Aquí está toda la lógica de establecimiento de estado migrada a una función reductora:
Debido a que la función reductora toma el estado (tasks) como argumento, puedesdeclararla fuera de tu componente.Esto disminuye el nivel de indentación y puede hacer que tu código sea más fácil de leer.
Nota
El código anterior usa sentencias if/else, pero es una convención usarsentencias switchdentro de los reductores. El resultado es el mismo, pero puede ser más fácil leer las sentencias switch de un vistazo.
Las usaremos a lo largo del resto de esta documentación de la siguiente manera:
Recomendamos envolver cada bloquecaseentre llaves{ y }para que las variables declaradas dentro de diferentescases no entren en conflicto entre sí. Además, uncasenormalmente debería terminar con unreturn. Si olvidas hacerreturn, el código “caerá” al siguientecase, lo que puede llevar a errores.
Si aún no te sientes cómodo con las sentencias switch, usar if/else está completamente bien.
Paso 3: Usa el reductor desde tu componente
Finalmente, necesitas conectar eltasksReducera tu componente. Importa el HookuseReducerdesde React:
Luego puedes reemplazaruseState:
conuseReducerasí:
El HookuseReduceres similar auseState—debes pasarle un estado inicial y devuelve un valor con estado y una forma de establecer el estado (en este caso, la función dispatch). Pero es un poco diferente.
El HookuseReducertoma dos argumentos:
- Una función reductora
- Un estado inicial
Y devuelve:
- Un valor con estado
- Una función dispatch (para “despachar” acciones del usuario al reductor)
¡Ahora está completamente conectado! Aquí, el reductor se declara al final del archivo del componente:
Si lo deseas, incluso puedes mover el reducer a un archivo diferente:
La lógica del componente puede ser más fácil de leer cuando separas las preocupaciones de esta manera. Ahora los controladores de eventos solo especificanqué sucedióal despachar acciones, y la función reductora determinacómo se actualiza el estadoen respuesta a ellas.
Comparación deuseState y useReducer
¡Los reducers no están exentos de desventajas! Aquí hay algunas formas en que puedes compararlos:
- Tamaño del código:Generalmente, con
useStatetienes que escribir menos código inicialmente. ConuseReducer, tienes que escribir tanto una función reductoracomoacciones de despacho. Sin embargo,useReducerpuede ayudar a reducir el código si muchos controladores de eventos modifican el estado de manera similar. - Legibilidad:
useStatees muy fácil de leer cuando las actualizaciones de estado son simples. Cuando se vuelven más complejas, pueden inflar el código de tu componente y dificultar su lectura. En este caso,useReducerte permite separar claramente elcómode la lógica de actualización delqué sucedióde los controladores de eventos. - Depuración:Cuando tienes un error con
useState, puede ser difícil saberdóndese estableció incorrectamente el estado, ypor qué. ConuseReducer, puedes agregar un registro en la consola en tu reducer para ver cada actualización de estado, ypor quésucedió (debido a quéaction). Si cadaactiones correcta, sabrás que el error está en la lógica del reducer en sí. Sin embargo, tienes que revisar más código que conuseState. - Pruebas:Un reducer es una función pura que no depende de tu componente. Esto significa que puedes exportarlo y probarlo por separado de forma aislada. Si bien generalmente es mejor probar componentes en un entorno más realista, para lógicas de actualización de estado complejas puede ser útil afirmar que tu reducer devuelve un estado particular para un estado inicial y una acción específicos.
- Preferencia personal:A algunas personas les gustan los reducers, a otras no. Eso está bien. Es cuestión de preferencia. Siempre puedes convertir entre
useStateyuseReducerde ida y vuelta: ¡son equivalentes!
Recomendamos usar un reducer si a menudo encuentras errores debido a actualizaciones de estado incorrectas en algún componente, y quieres introducir más estructura a su código. No tienes que usar reducers para todo: ¡siéntete libre de mezclar y combinar! Incluso puedes usaruseState y useReduceren el mismo componente.
Escribir reducers correctamente
Ten en cuenta estos dos consejos al escribir reducers:
- Los reductores deben ser puros.Similar a lasfunciones actualizadoras de estado, ¡los reductores se ejecutan durante el renderizado! (Las acciones se encolan hasta el siguiente renderizado). Esto significa que los reductoresdeben ser puros—las mismas entradas siempre producen la misma salida. No deben enviar solicitudes, programar tiempos de espera o realizar ningún efecto secundario (operaciones que afecten cosas fuera del componente). Deben actualizarobjetos y arrayssin mutaciones.
- Cada acción describe una única interacción del usuario, incluso si eso conduce a múltiples cambios en los datos.Por ejemplo, si un usuario presiona "Restablecer" en un formulario con cinco campos gestionados por un reductor, tiene más sentido enviar una acción
reset_formen lugar de cinco acciones separadasset_field. Si registras cada acción en un reductor, ese registro debe ser lo suficientemente claro como para que puedas reconstruir qué interacciones o respuestas sucedieron y en qué orden. ¡Esto ayuda con la depuración!
Escribir reductores concisos con Immer
Al igual que con laactualización de objetos y arraysen el estado regular, puedes usar la biblioteca Immer para hacer que los reductores sean más concisos. Aquí,useImmerReducerte permite mutar el estado conpusho asignaciones comoarr[i] =:
Los reductores deben ser puros, por lo que no deben mutar el estado. Pero Immer te proporciona un objeto especialdraftque es seguro mutar. Internamente, Immer creará una copia de tu estado con los cambios que hiciste en eldraft. Por eso los reductores gestionados poruseImmerReducerpueden mutar su primer argumento y no necesitan devolver el estado.
Recapitulación
- Para convertir de
useStateauseReducer:- Envía acciones desde los controladores de eventos.
- Escribe una función reductora que devuelva el siguiente estado para un estado y acción dados.
- Reemplaza
useStateconuseReducer.
- Los reductores requieren que escribas un poco más de código, pero ayudan con la depuración y las pruebas.
- Los reductores deben ser puros.
- Cada acción describe una única interacción del usuario.
- Usa Immer si quieres escribir reductores en un estilo de mutación.
Try out some challenges
Challenge 1 of 4:Dispatch actions from event handlers #
Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient.
Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js.
