Los Hooks se introdujeron en React a partir de la versión 16.8. Desde entonces, han permitido una mejor gestión del estado y la lógica en los componentes funcionales.
El hook useReducer
es una alternativa a useState
para manejar estados complejos. Se basa en el patrón de reducción, donde una función "reducer" recibe el estado actual y una acción, y devuelve un nuevo estado. Es una solución ligera en comparación con Redux o Flux y es ideal cuando un componente requiere múltiples actualizaciones de estado basadas en diferentes acciones.
Este es un ejemplo básico de useReducer
:
1const initialCounter = () => ({ contador: 0 }); 2const [state, dispatch] = useReducer(counterReducer, initialCounter());
El hook useReducer
recibe dos argumentos:
Reducer: Una función que determina cómo cambiar el estado según la acción recibida.
Estado inicial: Puede ser un objeto o una función que retorne un estado inicial.
El dispatch
es una función que permite enviar acciones para modificar el estado.
El reducer recibe el estado actual y una acción para generar un nuevo estado:
1function counterReducer(state, action) { 2 switch (action.type) { 3 case "INCREMENT": 4 return { ...state, contador: state.contador + 1 }; 5 case "DECREMENT": 6 return { ...state, contador: state.contador - 1 }; 7 case "PLUSTEN": 8 return { ...state, contador: state.contador + 10 }; 9 case "MULTIPLYBYTWO": 10 return { ...state, contador: state.contador * 2 }; 11 case "RESET": 12 return { ...state, contador: 0 }; 13 default: 14 return state; 15 } 16}
Cada acción es un objeto con una propiedad type, que define la operación a realizar. Si la acción no coincide con ningún caso, el reducer devuelve el estado sin cambios.
Cuando un estado es simple, useState
suele ser suficiente. Sin embargo, en casos donde hay múltiples acciones que afectan el estado, useReducer ayuda a mantener la lógica organizada y reutilizable.
Estamos acostumbrados a percibir los componentes como la unidad que agrupa la vista y la lógica para su funcionamiento. Por ejemplo, en el siguiente código hay un componente Counter
que tiene el HTML para definir cómo debe verse un contador numérico, y también la lógica de cómo debería sumar una unidad cada vez que se presiona el botón «+1».
1export default function Counter() { 2 // Lógica ⬇️ 3 const [counter, setCounter] = useState(0); 4 const increment = () => setCounter(counter + 1); 5 6 // Vista ⬇️ 7 return ( 8 <div className="container"> 9 <h2>State counter</h2> 10 <h3>{counter}</h3> 11 <div className="buttons"> 12 <button onClick={increment}>+1</button> 13 </div> 14 </div> 15 ); 16}
Pero, ¿qué pasa si necesitamos reutilizar la lógica en otros componentes? Podríamos hablar sobre estados centralizados, pero ¿qué pasa si solo quiero reutilizar la lógica, y que cada componente tenga su propio estado? Una solución menos práctica sería copiar y pegar o exportar funciones desde un archivo separado y hacer que funcionen con el estado de cada componente 😰. Eso no suena conveniente...
La solución a este problema es useReducer
, que, como su nombre indica, la función es reducir un estado y su lógica a una unidad reutilizable, permitiendo exportarlo desde un archivo a los componentes que lo necesiten 💪. Este reductor convivirá con el resto de la sintaxis típica de un componente React; puedes aprender más aquí.
Con useState
, un contador con varias acciones se vería como el siguiente ejemplo en el que tenemos un contador que no sólo se incrementa de 1 en 1 sino que además tiene otras opciones para modificar su valor.
1export default function CounterUsingState() { 2 const [counter, setCounter] = useState(0); 3 4 return ( 5 <div className="container"> 6 <h2>State counter</h2> 7 <h3>{counter}</h3> 8 <div className="buttons"> 9 <button onClick={() => setCounter(counter + 1)}>+1</button> 10 <button onClick={() => setCounter(counter - 1)}>-1</button> 11 <button onClick={() => setCounter(0)}>0</button> 12 <button onClick={() => setCounter(counter + 10)}>+10</button> 13 <button onClick={() => setCounter(counter * 2)}>x2</button> 14 </div> 15 </div> 16 ); 17}
Con useReducer, podemos trasladar la lógica a un archivo separado:
1// contadorReductor.js 2export const initialCounter = () => ({ contador: 0 }); 3 4export default function counterReducer(state, action) { 5 switch (action.type) { 6 case "INCREMENT": 7 return { ...state, contador: state.contador + 1 }; 8 case "DECREMENT": 9 return { ...state, contador: state.contador - 1 }; 10 case "PLUSTEN": 11 return { ...state, contador: state.contador + 10 }; 12 case "MULTIPLYBYTWO": 13 return { ...state, contador: state.contador * 2 }; 14 case "RESET": 15 return { ...state, contador: 0 }; 16 default: 17 return state; 18 } 19}
Y ahora en el componente usamos useReducer
:
1import React, { useReducer } from "react"; 2import counterReducer, { initialCounter } from "./counterReducer"; 3 4export default function CounterUsingReducer() { 5 const [state, dispatch] = useReducer(counterReducer, initialCounter()); 6 7 return ( 8 <div> 9 <h2>Reducer counter</h2> 10 <h3>{state.contador}</h3> 11 <div> 12 <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button> 13 <button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button> 14 <button onClick={() => dispatch({ type: "RESET" })}>0</button> 15 <button onClick={() => dispatch({ type: "PLUSTEN" })}>+10</button> 16 <button onClick={() => dispatch({ type: "MULTIPLYBYTWO" })}>x2</button> 17 </div> 18 </div> 19 ); 20}
Ahora la lógica del estado es completamente reutilizable en otros componentes.
Hemos visto las ventajas de useReducer y sabemos cómo extraer la lógica de nuestro estado a un reductor ubicado en un fichero externo que otros componentes pueden reutilizar. Esto no significa que tengas que descartar por completo useState
y usar sólo useReducer
; como todo en programación, se trata de usar la herramienta adecuada para el trabajo adecuado. Puedes aprender más sobre React y sus herramientas en esta categoría.
Los reductores son ideales cuando muchas funciones están asociadas con el estado, y agrupar la lógica y los datos es conveniente. Esto puede ocurrir en un escenario de gran complejidad o cuando funciones y estados necesitan ser reutilizados en múltiples componentes, entonces tendrás la poderosa herramienta de useReducer en tu arsenal.