Ir al contenido principal

Fundamentos de Redux, Parte 3: Estado, Acciones y Reductores

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Qué aprenderás
  • Cómo definir valores de estado que contengan los datos de tu aplicación
  • Cómo definir objetos de acción que describan lo que sucede en tu aplicación
  • Cómo escribir funciones reductoras que calculen el estado actualizado basándose en el estado actual y las acciones
Prerrequisitos

Introducción

En la Parte 2: Conceptos y Flujo de Datos de Redux, vimos cómo Redux nos ayuda a construir aplicaciones mantenibles al proporcionarnos un único lugar central para colocar el estado global de la aplicación. También hablamos sobre conceptos fundamentales de Redux como el envío de objetos de acción y el uso de funciones reductoras que devuelven nuevos valores de estado.

Ahora que tienes una idea de lo que son estas piezas, es hora de poner ese conocimiento en práctica. Vamos a construir una pequeña aplicación de ejemplo para ver cómo funcionan realmente juntas estas piezas.

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

precaución

Ten en cuenta que este tutorial muestra intencionadamente patrones de lógica de Redux de estilo antiguo que requieren más código que los patrones de "modern Redux" con Redux Toolkit que enseñamos como el enfoque correcto para construir aplicaciones con Redux hoy en día, con el fin de explicar los principios y conceptos detrás de Redux. No está pensado para ser un proyecto listo para producción.

Consulta estas páginas para aprender a usar "modern Redux" con Redux Toolkit:

Configuración del proyecto

Para este tutorial, hemos creado un proyecto inicial preconfigurado que ya tiene React instalado, incluye algunos estilos predeterminados y tiene una API REST falsa que nos permitirá escribir solicitudes API reales en nuestra aplicación. Usarás esto como base para escribir el código real de la aplicación.

Para comenzar, puedes abrir y hacer un fork de este CodeSandbox:

También puedes clonar el mismo proyecto desde este repositorio de Github. Después de clonar el repositorio, puedes instalar las herramientas del proyecto con npm install e iniciarlo con npm start.

Si quieres ver la versión final de lo que vamos a construir, puedes consultar la rama tutorial-steps o ver la versión final en este CodeSandbox.

Creación de un nuevo proyecto Redux + React

Una vez que termines este tutorial, probablemente querrás trabajar en tus propios proyectos. Te recomendamos usar las plantillas de Redux para Create-React-App como la forma más rápida de crear un nuevo proyecto Redux + React. Viene con Redux Toolkit y React-Redux ya configurados, usando una versión modernizada del ejemplo de la aplicación "contador" que viste en la Parte 1. Esto te permite comenzar directamente a escribir el código real de tu aplicación sin tener que añadir los paquetes de Redux ni configurar el store.

Si quieres conocer detalles específicos sobre cómo añadir Redux a un proyecto, consulta esta explicación:

Detailed Explanation: Adding Redux to a React Project

The Redux template for CRA comes with Redux Toolkit and React-Redux already configured. If you're setting up a new project from scratch without that template, follow these steps:

  • Add the @reduxjs/toolkit and react-redux packages
  • Create a Redux store using RTK's configureStore API, and pass in at least one reducer function
  • Import the Redux store into your application's entry point file (such as src/index.js)
  • Wrap your root React component with the <Provider> component from React-Redux, like:
root.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

Explorando el proyecto inicial

Este proyecto inicial se basa en la plantilla estándar de Vite, con algunas modificaciones.

Echemos un vistazo rápido al contenido inicial del proyecto:

  • /src
    • index.js: el archivo de entrada de la aplicación. Renderiza el componente principal <App>.
    • App.js: el componente principal de la aplicación.
    • index.css: estilos para la aplicación completa
    • /api
      • client.js: un pequeño cliente envoltorio de fetch que nos permite hacer peticiones HTTP GET y POST
      • server.js: proporciona una API REST falsa para nuestros datos. Nuestra aplicación obtendrá datos de estos endpoints falsos más adelante.
    • /exampleAddons: contiene algunos complementos adicionales de Redux que usaremos más adelante en el tutorial para mostrar cómo funcionan las cosas

Si cargas la aplicación ahora, deberías ver un mensaje de bienvenida, pero el resto de la aplicación está vacío.

¡Con esto, empecemos!

Comenzando la aplicación de ejemplo Todo

Nuestra aplicación de ejemplo será una pequeña aplicación de "tareas pendientes" (todo). Probablemente ya has visto ejemplos de aplicaciones de tareas antes; son buenos ejemplos porque nos permiten mostrar cómo hacer cosas como rastrear una lista de elementos, manejar la entrada del usuario y actualizar la interfaz cuando esos datos cambian, que son aspectos comunes en aplicaciones normales.

Definición de requisitos

Comencemos por definir los requisitos iniciales de negocio para esta aplicación:

  • La interfaz debe constar de tres secciones principales:

    • Una caja de entrada para que el usuario escriba el texto de un nuevo elemento de tarea
    • Una lista de todos los elementos de tarea existentes
    • Una sección de pie de página que muestra el número de tareas no completadas y opciones de filtrado
  • Los elementos de la lista de tareas deben tener una casilla de verificación que alterna su estado "completado". También debemos poder añadir una etiqueta de categoría codificada por colores (para una lista predefinida de colores) y eliminar elementos de tareas.

  • El contador debe pluralizar el número de tareas activas: "0 elementos", "1 elemento", "3 elementos", etc.

  • Debe haber botones para marcar todas las tareas como completadas y para eliminar todas las tareas completadas.

  • Debe haber dos formas de filtrar las tareas mostradas en la lista:

    • Filtrado basado en mostrar "Todas", "Activas" y "Completadas"
    • Filtrado basado en seleccionar uno o más colores, mostrando las tareas cuya etiqueta coincida con esos colores

Añadiremos más requisitos más adelante, pero esto es suficiente para comenzar.

El objetivo final es una aplicación que debería verse así:

Captura de pantalla de la aplicación de tareas de ejemplo

Diseño de los valores del estado

Uno de los principios fundamentales de React y Redux es que tu interfaz de usuario debe basarse en tu estado. Por lo tanto, un enfoque para diseñar una aplicación es pensar primero en todo el estado necesario para describir cómo funciona. También es buena idea intentar describir tu UI con la menor cantidad de valores posible en el estado, para reducir los datos que necesitas rastrear y actualizar.

Conceptualmente, hay dos aspectos principales en esta aplicación:

  • La lista real de elementos de tarea actuales

  • Las opciones de filtrado actuales

También necesitaremos rastrear los datos que el usuario escribe en la caja de entrada "Añadir tarea", pero esto es menos importante y lo manejaremos más adelante.

Para cada elemento de tarea, necesitamos almacenar varios datos:

  • El texto introducido por el usuario

  • El indicador booleano que muestra si está completado o no

  • Un valor de ID único

  • Una categoría de color, si está seleccionada

Nuestro comportamiento de filtrado puede describirse con algunos valores enumerados:

  • Estado de completado: "Todas", "Activas" y "Completadas"

  • Colores: "Rojo", "Amarillo", "Verde", "Azul", "Naranja", "Morado"

Al observar estos valores, podemos decir que las tareas son "estado de aplicación" (los datos principales con los que trabaja la aplicación), mientras que los valores de filtrado son "estado de UI" (estado que describe lo que la aplicación está haciendo actualmente). Es útil considerar estas categorías para entender cómo se usan las diferentes partes del estado.

Diseño de la estructura del estado

Con Redux, nuestro estado de aplicación siempre se mantiene en objetos y arrays simples de JavaScript. Esto significa que no puedes poner otras cosas en el estado de Redux: ni instancias de clases, tipos JS incorporados como Map/Set/Promise/Date, funciones, ni nada que no sean datos JS planos.

El valor raíz del estado de Redux casi siempre es un objeto simple de JS, con otros datos anidados dentro.

Con esta información, ya deberíamos poder describir los tipos de valores que necesitamos tener dentro del estado de Redux:

  • Primero, necesitamos un array de objetos de tareas pendientes. Cada elemento debe tener estos campos:

    • id: un número único
    • text: el texto que el usuario escribió
    • completed: un indicador booleano
    • color: Una categoría de color opcional
  • Luego, necesitamos describir nuestras opciones de filtrado. Debemos tener:

    • El valor actual del filtro "completed"
    • Un array de las categorías de color seleccionadas actualmente

Así que así es como podría verse un ejemplo del estado de nuestra aplicación:

const todoAppState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'Active',
colors: ['red', 'blue']
}
}

Es importante señalar que ¡está bien tener otros valores de estado fuera de Redux! Este ejemplo es lo suficientemente pequeño como para tener todo nuestro estado en el store de Redux, pero como veremos más adelante, algunos datos realmente no necesitan guardarse en Redux (como "¿está este desplegable abierto?" o "valor actual de un campo de formulario").

Diseño de acciones

Las acciones son objetos JavaScript simples que tienen un campo type. Como se mencionó anteriormente, puedes pensar en una acción como un evento que describe algo que sucedió en la aplicación.

De la misma manera que diseñamos la estructura del estado basándonos en los requisitos de la aplicación, también deberíamos ser capaces de crear una lista de algunas de las acciones que describen lo que está sucediendo:

  • Añadir una nueva tarea basada en el texto que el usuario introdujo

  • Cambiar el estado de completado de una tarea

  • Seleccionar una categoría de color para una tarea

  • Eliminar una tarea

  • Marcar todas las tareas como completadas

  • Limpiar todas las tareas completadas

  • Elegir un valor diferente para el filtro "completed"

  • Añadir un nuevo filtro de color

  • Eliminar un filtro de color

Normalmente colocamos cualquier dato adicional necesario para describir lo que sucede en el campo action.payload. Esto podría ser un número, una cadena de texto o un objeto con múltiples campos dentro.

Al store de Redux no le importa cuál sea el texto real del campo action.type. Sin embargo, tu propio código examinará action.type para ver si se necesita una actualización. Además, con frecuencia observarás las cadenas de tipo de acción en la extensión Redux DevTools mientras depuras para ver qué está sucediendo en tu aplicación. Por lo tanto, intenta elegir tipos de acción que sean legibles y que describan claramente lo que sucede: ¡será mucho más fácil entender las cosas cuando las revises más tarde!

Basándonos en esa lista de cosas que pueden suceder, podemos crear una lista de acciones que nuestra aplicación utilizará:

  • {type: 'todos/todoAdded', payload: todoText}

  • {type: 'todos/todoToggled', payload: todoId}

  • {type: 'todos/colorSelected', payload: {todoId, color}}

  • {type: 'todos/todoDeleted', payload: todoId}

  • {type: 'todos/allCompleted'}

  • {type: 'todos/completedCleared'}

  • {type: 'filters/statusFilterChanged', payload: filterValue}

  • {type: 'filters/colorFilterChanged', payload: {color, changeType}}

En este caso, las acciones tienen principalmente un único dato adicional, por lo que podemos ponerlo directamente en el campo action.payload. Podríamos haber dividido el comportamiento del filtro de color en dos acciones, una para "añadido" y otra para "eliminado", pero en este caso lo haremos como una acción con un campo adicional específicamente para mostrar que podemos tener objetos como payload de una acción.

Al igual que los datos del estado, las acciones deben contener la mínima cantidad de información necesaria para describir lo que sucedió.

Escritura de reductores

Ahora que conocemos la estructura de nuestro estado y cómo son nuestras acciones, es hora de escribir nuestro primer reducer.

Los reducers son funciones que toman el state actual y una action como argumentos, y devuelven un nuevo resultado de state. En otras palabras, (state, action) => newState.

Creación del reducer raíz

Una aplicación Redux realmente solo tiene una función reducer: el "root reducer" (reducer raíz) que luego pasarás a createStore. Esta única función reducer raíz es responsable de manejar todas las acciones despachadas y calcular cuál debe ser el completo nuevo estado cada vez.

Empecemos creando un archivo reducer.js en la carpeta src, junto a index.js y App.js.

Cada reducer necesita un estado inicial, así que añadiremos algunas tareas de ejemplo para comenzar. Luego, podemos escribir un esqueleto para la lógica dentro de la función reducer:

src/reducer.js
const initialState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'All',
colors: []
}
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}

Un reducer puede ser llamado con undefined como valor del estado cuando la aplicación se está inicializando. Si eso ocurre, debemos proporcionar un valor de estado inicial para que el resto del código del reducer tenga con qué trabajar. Normalmente los reducers usan la sintaxis de parámetros por defecto para proporcionar el estado inicial: (state = initialState, action).

A continuación, añadamos la lógica para manejar la acción 'todos/todoAdded'.

Primero necesitamos comprobar si el tipo de la acción actual coincide con esa cadena específica. Luego, necesitamos devolver un nuevo objeto que contenga todo el estado, incluso para los campos que no han cambiado.

src/reducer.js
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
case 'todos/todoAdded': {
// We need to return a new state object
return {
// that has all the existing state data
...state,
// but has a new array for the `todos` field
todos: [
// with all of the old todos
...state.todos,
// and the new todo object
{
// Use an auto-incrementing numeric ID for this example
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}

Eso... es muchísimo trabajo solo para añadir una tarea al estado. ¿Por qué es necesario todo este esfuerzo adicional?

Reglas de los reducers

Dijimos antes que los reducers deben siempre seguir algunas reglas especiales:

  • Deben calcular el nuevo valor de estado basándose únicamente en los argumentos state y action

  • No pueden modificar el state existente. En su lugar, deben realizar actualizaciones inmutables, copiando el state existente y haciendo cambios en los valores copiados

  • No deben realizar lógica asíncrona ni otros "efectos secundarios"

consejo

Un "efecto secundario" es cualquier cambio en el estado o comportamiento observable fuera de devolver un valor desde una función. Algunos tipos comunes de efectos secundarios son:

  • Registrar un valor en la consola
  • Guardar un archivo
  • Establecer un temporizador asíncrono
  • Hacer una petición HTTP
  • Modificar algún estado que existe fuera de una función, o mutar los argumentos de una función
  • Generar números aleatorios o IDs únicos aleatorios (como Math.random() o Date.now())

Cualquier función que siga estas reglas también se conoce como función "pura", incluso si no está escrita específicamente como función reducer.

Pero, ¿por qué son importantes estas reglas? Hay varias razones:

  • Uno de los objetivos de Redux es que tu código sea predecible. Cuando la salida de una función se calcula únicamente a partir de los argumentos de entrada, es más fácil entender cómo funciona ese código y probarlo.

  • Por otro lado, si una función depende de variables externas a ella misma o se comporta aleatoriamente, nunca se sabe qué pasará al ejecutarla.

  • Si una función modifica otros valores, incluidos sus argumentos, puede cambiar el funcionamiento de la aplicación de forma inesperada. Esto es una fuente común de errores, como "actualicé mi estado pero ahora mi interfaz no se actualiza cuando debería".

  • Algunas capacidades de Redux DevTools dependen de que tus reductores sigan estas reglas correctamente.

La regla sobre "actualizaciones inmutables" es particularmente importante y merece más atención.

Reductores y actualizaciones inmutables

Anteriormente, hablamos sobre "mutación" (modificar valores existentes de objetos/arrays) e "inmutabilidad" (tratar valores como algo que no puede cambiarse).

advertencia

En Redux, ¡los reducers nunca pueden mutar los valores de estado originales!

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

Hay varias razones por las que no debes mutar el estado en Redux:

  • Causa errores, como que la interfaz no se actualice correctamente para mostrar los últimos valores

  • Dificulta entender por qué y cómo se actualizó el estado

  • Complica la escritura de pruebas

  • Rompe la capacidad de usar correctamente la "depuración viaje en el tiempo"

  • Va en contra del espíritu y patrones de uso previstos para Redux

Entonces, si no podemos cambiar los originales, ¿cómo devolver un estado actualizado?

consejo

Los reductores solo pueden hacer copias de los valores originales y luego mutar esas copias.

// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}

Ya vimos que podemos escribir actualizaciones inmutables manualmente, usando los operadores de propagación de arrays/objetos de JavaScript y otras funciones que devuelven copias de los valores originales.

Esto se vuelve más difícil cuando los datos están anidados. Una regla crítica de las actualizaciones inmutables es que debes hacer una copia de cada nivel de anidamiento que necesite ser actualizado.

Sin embargo, si estás pensando que "escribir actualizaciones inmutables manualmente de esta manera parece difícil de recordar y hacer correctamente"... ¡sí, tienes razón! :)

Escribir lógica de actualización inmutable a mano es difícil, y mutar accidentalmente el estado en los reducers es el error más común que cometen los usuarios de Redux.

consejo

En aplicaciones reales, no tendrás que escribir manualmente estas complejas actualizaciones inmutables anidadas. En Parte 8: Redux moderno con Redux Toolkit, aprenderás a usar Redux Toolkit para simplificar la escritura de lógica de actualización inmutable en los reductores.

Manejo de acciones adicionales

Con esto en mente, añadamos la lógica del reductor para un par de casos más. Primero, alternar el campo completed de una tarea basándonos en su ID:

src/reducer.js
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
// Again copy the entire state object
...state,
// This time, we need to make a copy of the old todos array
todos: state.todos.map(todo => {
// If this isn't the todo item we're looking for, leave it alone
if (todo.id !== action.payload) {
return todo
}

// We've found the todo that has to change. Return a copy:
return {
...todo,
// Flip the completed flag
completed: !todo.completed
}
})
}
}
default:
return state
}
}

Y como hemos estado centrándonos en el estado de las tareas, añadamos también un caso para manejar la acción "cambio de selección de visibilidad":

src/reducer.js
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
...state,
todos: state.todos.map(todo => {
if (todo.id !== action.payload) {
return todo
}

return {
...todo,
completed: !todo.completed
}
})
}
}
case 'filters/statusFilterChanged': {
return {
// Copy the whole state
...state,
// Overwrite the filters value
filters: {
// copy the other filter fields
...state.filters,
// And replace the status field with the new value
status: action.payload
}
}
}
default:
return state
}
}

Solo hemos manejado 3 acciones, pero esto ya se está haciendo un poco largo. Si intentamos manejar todas las acciones en esta única función reductora, será difícil entenderlo todo.

Por eso los reductores normalmente se dividen en múltiples funciones reductoras más pequeñas, para facilitar la comprensión y el mantenimiento de la lógica del reductor.

División de reductores

Como parte de esto, los reductores de Redux típicamente se separan según la sección del estado de Redux que actualizan. Nuestro estado de la aplicación de tareas tiene actualmente dos secciones de primer nivel: state.todos y state.filters. Así que podemos dividir la gran función reductora raíz en dos reductores más pequeños: un todosReducer y un filtersReducer.

Entonces, ¿dónde deben ubicarse estas funciones reductoras divididas?

Recomendamos organizar las carpetas y archivos de tu aplicación Redux basándote en "características": código relacionado con un concepto o área específica de tu aplicación. El código Redux para una característica particular usualmente se escribe en un solo archivo, conocido como archivo "slice", que contiene toda la lógica del reductor y todo el código relacionado con acciones para esa parte del estado de tu aplicación.

Debido a esto, el reductor para una sección específica del estado de la aplicación Redux se llama "slice reducer". Normalmente, algunos objetos de acción estarán estrechamente relacionados con un slice reducer específico, por lo que las cadenas de tipo de acción deben comenzar con el nombre de esa característica (como 'todos') y describir el evento ocurrido (como 'todoAdded'), uniéndose en una sola cadena ('todos/todoAdded').

En nuestro proyecto, crea una nueva carpeta features, y dentro una carpeta todos. Crea un nuevo archivo llamado todosSlice.js, y cortemos y peguemos el estado inicial relacionado con las tareas en este archivo:

src/features/todos/todosSlice.js
const initialState = [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
]

function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}

export default function todosReducer(state = initialState, action) {
switch (action.type) {
default:
return state
}
}

Ahora podemos copiar la lógica para actualizar las tareas. Sin embargo, hay una diferencia importante aquí. ¡Este archivo solo tiene que actualizar el estado relacionado con las tareas - ya no está anidado! Esta es otra razón para dividir los reductores. Como el estado de las tareas es un array por sí mismo, no tenemos que copiar el objeto de estado raíz externo aquí. Esto facilita la lectura de este reductor.

Esto se llama composición de reductores, y es el patrón fundamental para construir aplicaciones Redux.

Así es como se ve el reductor actualizado después de manejar esas acciones:

src/features/todos/todosSlice.js
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
// Can return just the new todos array - no extra object around it
return [
...state,
{
id: nextTodoId(state),
text: action.payload,
completed: false
}
]
}
case 'todos/todoToggled': {
return state.map(todo => {
if (todo.id !== action.payload) {
return todo
}

return {
...todo,
completed: !todo.completed
}
})
}
default:
return state
}
}

Es un poco más corto y fácil de leer.

Ahora podemos hacer lo mismo con la lógica de visibilidad. Crea src/features/filters/filtersSlice.js y movamos todo el código relacionado con los filtros allí:

src/features/filters/filtersSlice.js
const initialState = {
status: 'All',
colors: []
}

export default function filtersReducer(state = initialState, action) {
switch (action.type) {
case 'filters/statusFilterChanged': {
return {
// Again, one less level of nesting to copy
...state,
status: action.payload
}
}
default:
return state
}
}

Todavía tenemos que copiar el objeto que contiene el estado de los filtros, pero como hay menos anidamiento, es más fácil leer lo que ocurre.

información

Para mantener esta página más corta, omitiremos mostrar cómo escribir la lógica de actualización del reductor para las otras acciones.

Intenta escribir las actualizaciones tú mismo, basándote en los requisitos descritos anteriormente.

Si te quedas atascado, consulta el CodeSandbox al final de esta página para ver la implementación completa de estos reductores.

Combinando reductores

Ahora tenemos dos archivos de slice separados, cada uno con su propia función reductora de slice. Pero, como dijimos antes, el almacén de Redux necesita una función reductora raíz al crearlo. Entonces, ¿cómo podemos volver a tener un reductor raíz sin poner todo el código en una gran función?

Dado que los reductores son funciones normales de JS, podemos importar los reductores de slice nuevamente en reducer.js y escribir un nuevo reductor raíz cuyo único trabajo sea llamar a las otras dos funciones.

src/reducer.js
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

export default function rootReducer(state = {}, action) {
// always return a new object for the root state
return {
// the value of `state.todos` is whatever the todos reducer returns
todos: todosReducer(state.todos, action),
// For both reducers, we only pass in their slice of the state
filters: filtersReducer(state.filters, action)
}
}

Ten en cuenta que cada uno de estos reductores gestiona su propia parte del estado global. El parámetro state es diferente para cada reductor y corresponde a la parte del estado que gestiona.

Esto nos permite dividir nuestra lógica según características y porciones del estado, manteniendo el código mantenible.

combineReducers

Podemos ver que el nuevo reductor raíz hace lo mismo para cada slice: llama al reductor de slice, le pasa la porción del estado que gestiona ese reductor y asigna el resultado de vuelta al objeto de estado raíz. Si añadiéramos más slices, el patrón se repetiría.

La biblioteca principal de Redux incluye una utilidad llamada combineReducers que realiza este paso repetitivo por nosotros. Podemos reemplazar nuestro rootReducer escrito manualmente por uno más breve generado por combineReducers.

Ahora que necesitamos combineReducers, es hora de instalar realmente la biblioteca principal de Redux:

npm install redux

Una vez hecho esto, podemos importar combineReducers y usarlo:

src/reducer.js
import { combineReducers } from 'redux'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
})

export default rootReducer

combineReducers acepta un objeto donde los nombres de las claves se convertirán en las claves de tu objeto de estado raíz, y los valores son las funciones reductoras de slice que saben cómo actualizar esas porciones del estado de Redux.

Recuerda: ¡Los nombres de clave que das a combineReducers determinan cómo serán las claves de tu objeto de estado raíz!

Lo que has aprendido

El Estado, las Acciones y los Reductores son los bloques fundamentales de Redux. Cada aplicación Redux tiene valores de estado, crea acciones para describir lo que ocurrió y utiliza funciones reductoras para calcular nuevos valores de estado basándose en el estado anterior y una acción.

Aquí está el contenido de nuestra aplicación hasta ahora:

Resumen
  • Las aplicaciones Redux usan objetos JS planos, arrays y valores primitivos como valores de estado
    • El valor de estado raíz debe ser un objeto JS plano
    • El estado debe contener la mínima cantidad de datos necesaria para que la app funcione
    • Clases, Promesas, funciones y otros valores no planos no deben ir en el estado de Redux
    • Los reductores no deben crear valores aleatorios como Math.random() o Date.now()
    • Es aceptable tener otros valores de estado fuera del almacén de Redux (como estado local de componentes) junto con Redux
  • Las acciones son objetos planos con un campo type que describe lo ocurrido
    • El campo type debe ser un string legible, y normalmente se escribe como 'feature/eventName'
    • Las acciones pueden contener otros valores, típicamente almacenados en el campo action.payload
    • Las acciones deben tener la mínima cantidad de datos necesaria para describir lo ocurrido
  • Los reductores son funciones con la forma (state, action) => newState
    • Los reductores siempre deben seguir reglas especiales:
      • Solo calcular el nuevo estado basándose en los argumentos state y action
      • Nunca mutar el state existente - siempre devolver una copia
      • Sin "efectos secundarios" como peticiones HTTP o lógica asíncrona
  • Los reductores deben dividirse para facilitar su lectura
    • Normalmente se dividen por claves de estado de nivel superior o "slices" del estado
    • Normalmente se escriben en archivos de "slice", organizados en carpetas de "características"
    • Los reductores pueden combinarse con la función combineReducers de Redux
    • Los nombres de clave dados a combineReducers definen las claves del objeto de estado de nivel superior

¿Qué sigue?

Ahora tenemos lógica de reductores que actualizará nuestro estado, pero esos reductores no harán nada por sí mismos. Necesitan colocarse dentro de un almacén de Redux, que pueda llamar al código del reductor con acciones cuando ocurra algo.

En Parte 4: Store, veremos cómo crear un store de Redux y ejecutar nuestra lógica del reducer.