Los hooks empezaron a existir en react desde la versión 16.8. Desde entonces, toda la arquitectura de react se ha transformado en una serie de "Hooks" que permiten implementar la mayoría de los patrones de programación mas importantes.
El useReducer es una propuesta de React para separar la lógica de la vista en tus componentes. Hay otras soluciones como Redux, Flux, Global Context, etc. Sin embargo, el useReducer se ha vuelto popular por ser sencillo de usar y mantener un alcance local sobre los datos, es decir, a pesar de reutilizar las funciones y código de los componentes, no se compartirán los datos entre sí.
Separar los datos de los componentes ayudar a prevenir errores comunes y reutilizar la información y la lógica en la aplicación.
El primer paso es declarar una función reducer (en este ejemplo se llama counterReducer
) que se define con 2 parámetros: El state
que contiene los datos del reducer, y un objeto actions
que se usa para identificar las acciones que podemos ejecutar para manipular el state.
1function counterReducer(state , action = {}) { 2 // Aquí el reducer recibe el estado actual 3 // luego ejecuta las acciones 4 // por ultimo retorna el estado nuevo 5}
Esta función reducer se encarga de mutar (o "modificar") el estado de tu componente en función de los tipos de acciones predefinidas, y deberá retornar una nueva versión del estado que reemplaza por completo la anterior al terminar su ejecución, por lo que hay que ser cuidadoso y sólo alterar lo que necesitamos y retornar siempre los demás valores del estado utilizando la desestructuracion (js destructuring) 🤓.
1function counterReducer(state , action = {}) { 2 // Hagas lo que hagas, siempre retorna un nuevo estado 3 4 //👍**SI** 5 return { ...state, counter: state.counter + 1 } 6 7 //🚫**NO** 8 //return { counter: state.counter + 1 } 9}
Esta función se utiliza como primer parámetro del hook useReducer
. Como segundo parámetro se debe pasar un objeto con los valores iniciales del estado.
El llamado al hook retorna un arreglo de dos valores que representan al nuevo estado (state
) y el dispatcher: El objeto que llama la ejecución de las acciones de la lógica del reducer (actions
).
1 const intialCounter={counter: 0}: 2 const [state, dispatch] = useReducer(counterReducer, initialCounter);
Dentro de la función reducer, el objeto actions
contiene una propiedad type
que nos indica qué acción ha sido invocada, y podremos escribir la lógica para mutar el estado por completo en función a esta propiedad.
1export default function counterReducer(state, action = {}) { 2 switch (action.type) { 3 case "INCREMENT": 4 return { ...state, counter: state.counter + 1 }; 5 case "DECREMENT": 6 return { ...state, counter: state.counter - 1 }; 7 case "PLUSTEN": 8 return { ...state, counter: state.counter + 10 }; 9 case "MULTYPLYBYTWO": 10 return { ...state, counter: state.counter * 2 }; 11 case "RESET": 12 return { ...state, counter: 0 }; 13 default: 14 // En caso no tener ningún tipo se retorna el estado sin alterar 15 throw Error("No se encuentra la acción especificada") 16 } 17}
Ademas de las acciones especificadas, se coloca un caso default
que se ejecuta cuando el tipo de acción no esta definido, para lo cual se arroja un error que interrumpe la aplicación. Esto puede parecer un poco extremo, pero es mejor tener un error visible y depurarlo, que tener una aplicación sin errores(🐞bugs) pero que no funciona como debería.
Ya con esto podemos tener tanto las funciones counterReducer
y el estado inicial initialCounter
exportadas en un archivo, para ser utilizadas por cualquier otro componente 👌.
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 como debería verse un contador de números y también existe la lógica de como sumar una unidad cada vez que se presione el botón "+1"
1export default function Counter() { 2 3 // Lógica ⬇️ 4 const [counter, setCounter] = useState(0); 5 const increment = () => setCounter(counter + 1); 6 7 // Vista ⬇️ 8 return ( 9 <div className="container"> 10 <h2>State counter</h2> 11 <h3>{counter}</h3> 12 <div className="buttons"> 13 <button onClick={increment}>+1</button> 14 </div> 15 </div> 16 ); 17}
Pero ¿Qué pasa si necesitamos reutilizar sólo la lógica en otros componentes? Podríamos hablar de estados centralizados, pero ¿Qué pasa si sólo quiero reutilizar la lógica y que cada componente tenga un estado propio? Una solución poco práctica seria copiar y pegar, o exportar las funciones desde un archivo aparte y buscar alguna manera de hacerlas trabajar con el estado de cada componente 😰. Eso no suena conveniente...
Una solución a este problema es useReducer
, que como dice su nombre, su función es reducir un estado y su lógica a una unidad reutilizable, permitiendo que esta se pueda exportar desde un archivo a los componentes que lo necesiten 💪. Este reducer va a coexistir con el resto de la sintaxis típica de un componente React, puedes aprender más aquí.
En este ejemplo tenemos un contador que no solamente suma de 1 en 1, sino también tiene otras opciones para modificar su valor.
Para realizar todas estas acciones se necesitan funciones para cada una de ellas, ademas del estado en si. Para eso usaremos el clásico hook useState
, aprende mas aquí.
1export default function CounterUsingState() { 2 const [counter, setCounter] = useState(0); 3 const increment = () => setCounter(counter + 1); 4 const decrement = () => setCounter(counter - 1); 5 const reset = () => setCounter(0); 6 const plusten = () => setCounter(counter + 10); 7 const multiplyByTwo = () => setCounter(counter * 2); 8 9 return ( 10 <div className="container"> 11 <h2>State counter</h2> 12 <h3>{counter}</h3> 13 <div className="buttons"> 14 <button onClick={increment}>+1</button> 15 <button onClick={decrement}>-1</button> 16 <button onClick={reset}>0</button> 17 <button onClick={plusten}>+10</button> 18 <button onClick={multiplyByTwo}>x2</button> 19 </div> 20 </div> 21 ); 22}
Esto funciona perfecto, pero para hacer la lógica reutilizable y moverlo a otro archivo, lo convertiremos en un reducer:
1// counterReducer.js 2export const initialCounter = {counter: 0}; 3export default function counterReducer(state, action = {}) { 4 switch (action.type) { 5 case "INCREMENT": 6 return { ...state, counter: state.counter + 1 }; 7 case "DECREMENT": 8 return { ...state, counter: state.counter - 1 }; 9 case "PLUSTEN": 10 return { ...state, counter: state.counter + 10 }; 11 case "MULTYPLYBYTWO": 12 return { ...state, counter: state.counter * 2 }; 13 case "RESET": 14 return { ...state, counter: 0 }; 15 default: 16 return state; 17 } 18} 19
Ahora desde el componente importamos y hacemos uso del reducer:
1import React, { useReducer } from "react"; 2import counterReducer, { initialCounter } from "./counterReducer"; 3 4export default function CounterUsingReducer() { 5 // Agregamos el hook useReducer, pasándole como parámetros 6 // nuestra función reducer y el inicializador, 7 // ambos importados desde otro archivo 8 const [state, dispatch] = useReducer(counterReducer, initialCounter); 9 10 return ( 11 <div> 12 <h2>Reducer counter</h2> 13 {/* Ahora el counter esta dentro del estado del reducer */} 14 <h3>{state.counter}</h3> 15 <div> 16 17 {/* Llamamos a la función dispatch con el tipo de acción para ejecutar la lógica del reducer */} 18 <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button> 19 <button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button> 20 <button onClick={() => dispatch({ type: "RESET" })}>0</button> 21 <button onClick={() => dispatch({ type: "PLUSTEN" })}>+10</button> 22 <button onClick={() => dispatch({ type: "MULTYPLYBYTWO" })}>x2</button> 23 </div> 24 </div> 25 ); 26}
Para que esto funcione fue necesario usar el state del reducer y reemplazar las funciones que estaban antes, por llamados a la función dispatch
, que ejecuta la lógica del reducer y recibe como parámetro el tipo de la acción que se va a ejecutar.
Ya hemos visto las ventajas de useReducer y sabemos como extraer la lógica de nuestro estado a un reducer ubicado en un archivo externo que pueden reutilizar los demás componentes. Esto no significa que tengas que desechar useState
por completo y solo usar useReducer
, como todo en programación se trata de usar la herramienta adecuada para el trabajo adecuado. Puedes aprender más de React y las herramientas que tiene en esta categoría
Los reducer son ideales cuando tenemos muchas funciones asociadas al estado, y nos convenga agrupar lógica y datos. Esto puede darse en un escenario de gran complejidad o cuando se necesite reutilizar funciones y estados en varios componentes, ahi tendrás la poderosa herramienta de useReducer en tu arsenal.