Hook para estados complejos useReducer

Juan Correa
10 min readOct 11, 2020
react hooks usereducer

Este post va a abarcar el hook useReducer y varios conceptos necesarios que si no estás familiarizado con ellos, puede que te sientas un poco abrumado.

Pero descuida, he hecho mi mejor esfuerzo para ponerla fácil y que al final del capítulo logres dominar este hook.

Si ya estás familiarizado con redux, será bastante simple de entender y siéntete libre de brincar hasta la parte del ejemplo de useReducer.

Dicho lo anterior, comencemos :).

El hook useReducer nos permite manejar estados “complejos” por medio de una función reductora.

¿Y qué es un estado “complejo”?

Un estado complejo es aquel que tiene una estructura de varios niveles, por ejemplo: un objeto de objetos que contiene arrays.

 1 {
2 foo: {
3 faa: {
4 test: [],
5 testB: ''
6 }
7 },
8 fee: [],
9 fii: {
10 testC: [],
11 testD: {}
12 }
13 }

En caso de que requieras un estado como el ejemplo anterior y no sea viable dividirlo usando diferentes useState, lo recomendado es que uses useReducer.

¿Y cómo luce este hook?

1 const [state, dispatch] = useReducer(
2 reducer,
3 initialArg,
4 init
5 );

Donde reducer es simplemente una función de Javascript que actualiza el estado, initialArg es el estado inicial e init es una función opcional para también definir el estado inicial.

Este hook devuelve un array donde en la posición cero es el estado y en la posición uno es un dispatch que es una función que usaremos para actualizar el estado.

En realidad, es muy parecido al hook useState:

1 const initialState = false;
2 const [state, setState] = useState(initialState);

Veamos más sobre el hook useReducer haciendo una comparación con redux y ejemplos sobre su uso en los siguientes apartados.

Funciones reductoras (reducers)

Un reducer es una función de Javascript que recibe por parámetro el valor del estado actual y por segundo parámetro un objeto plano de Javascript llamado actioncon la información necesaria para actualizar el estado.

Kha???

Suena más complejo de lo que parece. Mira el siguiente ejemplo de un reducer:

 1 function reducer(state, action) {
2 switch (action.type) {
3 case 'increment':
4 return {count: state.count + 1};
5 case 'decrement':
6 return {count: state.count - 1};
7 default:
8 throw new Error();
9 }
10 }

Como podemos ver, el reducer recibe el valor del estado y un action.

El parámetro action es un objeto plano de Javascript y tendrá por lo menos una propiedad llamada type que usamos como una cadena, ejemplo: { type: 'ADD_TODO' }.

Según el valor de action.type, es el cambio de estado a ejecutar.

Es importante que sepas que no mutamos el estado sino que creamos y devolvemos uno nuevo.

Hay quienes prefieren evitar el uso de switch pero debes saber que es opcional usarlo. Puedes usar una serie de sentencias if también. Depende de ti.

Este ejemplo de la documentación oficial de reactjs.org es sólo una muestra de cómo podría lucir un reducer para un estado de un contador, pero es sólo para fines de ejemplo.

Al inicio comentamos que useReducer es recomendado para estados complejos y es lo que veremos en el ejemplo práctico.

Ejemplo de un componente con useReducer

Vamos a hacer un componente para manejar una lista de cosas por hacer (TODO list).

Antes de implementar useReducer primero vamos a definir el estado inicial y la función reductora que nos pide este hook al ejecutarlo.

Primero hay que pensar en cuál es el estado más simple que puede necesitar nuestro componente de cosas por hacer.

Consideremos lo siguiente:

  • El usuario va a poder agregar nuevos elementos en la lista de cosas por hacer.
  • El usuario necesita visualizar en la UI la lista de las cosas por hacer.
  • El usuario va a poder marcar como completado o no completado un elemento de lista.

Te propongo que usemos la siguiente estructura:

1 const state = [
2 {
3 id: 1,
4 name: "Terminar de leer el capítulo de useReducer",
5 isCompleted: false
6 }
7 ];

Un array de objetos planos en el que cada objeto tendrá un id para identificarlo (podríamos usar el índice del array pero no es lo común en aplicaciones reales), un name y un booleano isCompleted que usaremos para marcar como completado o no completado.

Ahora hagamos nuestro reducer incluyendo la lógica necesaria para agregar un nuevo elemento de nuestro TODO solamente de momento.

 1 const reducer = (state, action) => {
2 if (action.type === "ADD_TODO") {
3 const { name } = action.payload;
4
5 return [
6 ...state,
7 {
8 id: uuidv4(),
9 name,
10 isCompleted: false
11 }
12 ];
13 }
14 };

Como es común, el reducer recibe el state y action. Estos valores serán pasados internamente por useReducer.

Lo que nos debemos preocupar aquí es en colocar la lógica para actualizar el estado, cosa que hacemos en el primer if.

Asumimos que tendremos un action.type igual a "ADD_TODO" y cuando lo recibamos vamos a retornar el nuevo estado.

Para retornar el nuevo estado, vamos a usar el spread operator para crear un nuevo array agregando todos los valores actuales de state, ya que state es un array.

Después agregamos el nuevo objeto con el id (usamos una biblioteca llamada uuid para que nos genere los id únicos), el name que proviene de action.payload e isCompletedcomo false por default.

Ahora vamos a agregar la lógica para actualizar isCompleted de un elemento del TODO en particular.

Veremos únicamente la parte correspondiente dentro del reducer.

 1   // dentro del reducer, este IF nuevo
2 if (action.type === "TOGGLE_IS_COMPLETED") {
3 const { id } = action.payload;
4
5 const newState = state.map((singleTodo) => {
6 if (singleTodo.id === id) {
7 return {
8 ...singleTodo,
9 isCompleted: !singleTodo.isCompleted
10 };
11 }
12
13 return singleTodo;
14 });
15
16 return newState;
17 }

Esta lógica es muy diferente a la de agregar. No hacemos spread ni nada por el estilo.

Primero obtenemos el id del elemento a actualizar por medio de action.payload, después creamos el nuevo estado por medio de state.map (.map retorna un nuevo array).

Dentro de .map está la parte importante.

Lo que hacemos aquí es comparar elemento por elemento para saber si el elemento de la lista coincide con el id que nos interesa actualizar.

Si coincide, retornamos todo lo que contenga ese elemento de la lista y su propiedad isCompleted le asignamos el valor contrario que tenga en ese momento.

Ejemplo:

 1 // Si...
2 singleTodo.isCompleted = true
3
4 // Entonces...
5 !singleTodo.isCompleted // igual a false
6
7 // y viceversa
8
9 // Si...
10 singleTodo.isCompleted = false
11
12 // Entonces...
13 !singleTodo.isCompleted // igual a true

Si no coincide con el id, simplemente retornamos el elemento sin modificarlo.

Al final retornamos el nuevo estado con su valor modificado.

Nuestro reducer completo luce del siguiente modo:

 1 const reducer = (state, action) => {
2 if (action.type === "ADD_TODO") {
3 const { name } = action.payload;
4
5 return [
6 ...state,
7 {
8 id: uuidv4(),
9 name,
10 isCompleted: false
11 }
12 ];
13 }
14
15 if (action.type === "TOGGLE_IS_COMPLETED") {
16 const { id } = action.payload;
17
18 const newState = state.map((singleTodo) => {
19 if (singleTodo.id === id) {
20 return {
21 ...singleTodo,
22 isCompleted: !singleTodo.isCompleted
23 };
24 }
25
26 return singleTodo;
27 });
28
29 return newState;
30 }
31
32 return state;
33 };

Ahora que tenemos estos elementos definidos, pasemos a crear nuestro componente :)

 1 const Todo = () => {
2 const [todoText, setTodoText] = useState("");
3
4 const handleChange = ({ target }) =>
5 setTodoText(target.value);
6
7 return (
8 <>
9 <p>
10 Nuevo TODO:
11 <input
12 type="text"
13 value={todoText}
14 onChange={handleChange}
15 />
16 <button>Agregar</button>
17 </p>
18
19 <h2>Listado</h2>
20 <ul>
21 <li>Test</li>
22 </ul>
23 </>
24 );
25 };

Tenemos el input para escribir el nuevo TODO y lo controlamos con un estado usando useState.

El listado está “hardcodeado” sólo para tener una idea de cómo mostraremos los elementos.

Ahora pasemos a agregar el useReducer :)

1 // dentro del componente agregamos:
2 const [todoText, setTodoText] = useState("");
3
4 // AQUI
5 const [state, dispatch] = useReducer(reducer, initialState);
6
7 // ...resto del código

Y modificamos la parte del render para que muestre los valores de nuestro state. Recuerda que pusimos un valor inicial en las cosas por hacer, por lo que será lo que se renderice en la lista.

 1 <ul>
2 {
3 state.map(({ name, isCompleted, id }) => {
4 const style = {
5 textDecoration: isCompleted
6 ? "line-through" : "inherit"
7 };
8
9 return (
10 <li key={id} style={style}>
11 {name}
12 </li>
13 );
14 })
15 }
16 </ul>

De momento, nuestro componente luce del siguiente modo:

 1 const Todo = () => {
2 const [todoText, setTodoText] = useState("");
3 const [state, dispatch] = useReducer(reducer, initialState);
4
5 const handleChange = ({ target }) =>
6 setTodoText(target.value);
7
8 return (
9 <>
10 <p>
11 Nuevo TODO:
12 <input
13 type="text"
14 value={todoText}
15 onChange={handleChange}
16 />
17 <button>Agregar</button>
18 </p>
19
20 <h2>Listado</h2>
21
22 <ul>
23 {state.map(({ name, isCompleted, id }) => {
24 const style = {
25 textDecoration: isCompleted
26 ? "line-through" : "inherit"
27 };
28
29 return (
30 <li key={id} style={style}>
31 {name}
32 </li>
33 );
34 })}
35 </ul>
36 </>
37 );
38 };

Ahora vamos a colocar la lógica para que al dar click en el botón “Agregar”, nos agregue el nuevo elemento en nuestro estado.

 1   const handleClick = () => {
2 dispatch({
3 type: "ADD_TODO",
4 payload: { name: todoText }
5 });
6 setTodoText("");
7 };
8
9 // en el botón
10 <button onClick={handleClick}>Agregar</button>

Ejecutamos el dispatch para actualizar el estado. Le pasamos el type y el payload necesarios como se ve en el ejemplo.

Al final, reseteamos el valor de todoText para limpiar el contenido del input.

Por último, vamos a agregar la capacidad de marcar un elemento como completado o no completado al dar click a un elemento del listado.

 1   const handleToggle = (id) => {
2 dispatch({
3 type: "TOGGLE_IS_COMPLETED",
4 payload: { id }
5 });
6 };
7
8 // en el elemento li
9 return (
10 <li
11 key={id}
12 style={style}
13 onClick={() => handleToggle(id)}
14 >
15 {name}
16 </li>
17 );

El código completo luce del siguiente modo:

1 const Todo = () => {
2 const [todoText, setTodoText] = useState("");
3 const [state, dispatch] = useReducer(reducer, initialState);
4
5 const handleChange = ({ target }) =>
6 setTodoText(target.value);
7
8 const handleClick = () => {
9 dispatch({
10 type: "ADD_TODO",
11 payload: { name: todoText }
12 });
13 setTodoText("");
14 };
15
16 const handleToggle = (id) => {
17 dispatch({
18 type: "TOGGLE_IS_COMPLETED",
19 payload: { id }
20 });
21 };
22
23 return (
24 <>
25 <p>
26 Nuevo TODO:
27 <input
28 type="text"
29 value={todoText}
30 onChange={handleChange}
31 />
32 <button onClick={handleClick}>Agregar</button>
33 </p>
34
35 <h2>Listado</h2>
36
37 <ul>
38 {state.map(({ name, isCompleted, id }) => {
39 const style = {
40 textDecoration: isCompleted
41 ? "line-through" : "inherit"
42 };
43
44 return (
45 <li
46 key={id}
47 style={style}
48 onClick={() => handleToggle(id)}
49 >
50 {name}
51 </li>
52 );
53 })}
54 </ul>
55 </>
56 );
57 };

Con esto hemos terminado nuestro ejemplo :D

Hook useReducer VS Redux

El nombre de useReducer da pie a confundirlo con redux, pero en realidad son cosas completamente diferentes.

  • useReducer es un hook de React para actualizar un estado interno por medio de una función llamada reducer.
  • redux es una arquitectura que nos permite abstraer el manejo de un estado global en una aplicación.

Redux es algo más complejo que abarca el nivel de decisiones de arquitectura de una aplicación, siendo react-redux una biblioteca que nos permite integrarlo fácilmente en React.

La confusión puede caer en el uso de funciones llamadas reducers que son usadas como una parte del funcionamiento de redux.

Descarga Ebook React Hooks Manual desde cero gratis

Este post es un extracto del ebook publicado en Amazon: React Hooks Manual Desde Cero. Lo puedes descargar GRATIS a través de mi sitio web 🤓.

🔔 Bonus: ¿Te gustaría dar un paso al siguiente nivel?

Recapitulación del hook useReducer

En este capítulo hemos visto lo siguiente:

  • Los conceptos de reducer, action y su funcionamiento con useReducer.
  • Un ejemplo práctico de un TODO aplicando useReducer.
  • Diferencias entre redux y useReducer.

Espero que este capítulo te haya dado luces para que puedas comenzar a implementar este hook en tus desarrollos cuando lo consideres necesario.

¿Te ha gustado el contenido?

claps for useReducer

Te invito a darle clap (1, 10, 50, los que quieras!)👏 y compartirlo! Puedes subscribirte a mi canal de YouTube y al blog de Developero para más contenidos de este tipo ⚡️🤘.

--

--

Juan Correa

Full Stack JS Engineer — Developero founder — Software Development Ebooks author. https://developero.io