Hook para estados complejos 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 action
con 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 isCompleted
como 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 conuseReducer
. - Un ejemplo práctico de un TODO aplicando
useReducer
. - Diferencias entre
redux
yuseReducer
.
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?
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 ⚡️🤘.