UseReducer como alternativa a Redux

Cómo emplear el hook useReducer como alternativa a la librería Redux

Image for post
Image for post

Es innegable la popularización de Redux desde que Dan Abravov allá por 2015 como librería para gestionar el estado de nuestra aplicación a partir del concepto de única fuente de verdad.

Sin embargo, pese a los innegables beneficios que reporta su uso, también es cierto que requiere el uso de código bastante repetitivo cada vez que queremos añadir un nuevo trozo de estado; a saber:

  • Tipos de acciones

Por supuesto, si queremos trabajar con acciones asíncronas esto se complica aún más, viéndonos obligados a emplear librerías como redux-thunk o redux-saga que nos permitan procesar este tipo de acciones.

Image for post
Image for post

Sin embargo, con la llegada de los React Hooks se nos ha proporcionado una nueva herramienta que nos servirá para simplificar nuestro código y ahorrarnos emplear Redux en según qué casos: useReducer .

Tal y como la documentación nos dice, useReducer :

An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

Es decir, este hook se nos presenta como alternativa al hook useState pues nos va a permitir aislar la lógica de gestión del estado del componente, obteniendo un código mucho más sencillo de mantener y permitiendo que los componentes se se centren en su verdadera función: “pintar elementos” (recordad que cuando empleamos el hook useState se delega al componente toda la lógica de gestión del estado, siendo el encargado de calcular el nuevo estado y de actualizarlo).

Por otra parte, y como no podía ser de otra manera, useReducer se inspira en la librería Redux para proporcionarnos una manera de gestionar el estado por medio del uso de acciones para actualizarlo (patrón procedente de Flux). Sin embargo, la principal diferencia entre Redux y este hook es que si bien Redux nos proporciona un estado global para toda la aplicación, useReducer parece estar más enfocado al estado local de un componente.

Sí, es cierto que useReducer no nos proporciona un estado global para toda la aplicación, pero React ya nos proporciona una forma de gestionar dicho estado mediante el componente Context :

que, atendiendo a su definición:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

Y que además cuenta con su propio hook: useContext para que podamos trabajar con este componente cuando escribamos componentes funcionales.

Por tanto, es posible escribir una aplicación que, valiéndose de estos dos hooks y el componente Context gestione el estado del mismo modo que hace Redux pero sin todo el boilerplate asociado. Veamos cómo.

Punto de partida

Imaginad que tenemos una aplicación contador (similar a la que la propia documentación de React emplea para introducirnos a los hooks) implementada del siguiente modo:

Es decir, tenemos:

  • Un componente Display para mostrar el valor del contador

Centralizando el estado

Lo primero que haremos será hacer global el valor del contador de modo que no tengamos que pasarlo mediante props al componente Display . Para ello emplearemos el hook useContext así como el propio componente Context :

Sin embargo, si probáis este código observaréis como hemos perdido la funcionalidad de incrementar o decrementar el contador, ya que tal y como hemos planteado la estructura, no podemos actualizar el contexto desde sus consumers. Pero para todo existe una solución.

useReducer

Cuando empleamos el hook useReducer obtenemos un array de dos elementos:

  • el primero contiene el estado

Por tanto, si queremos poder actualizar de forma global el contexto, emplearemos una combinación de este hook y de useContext para lograrlo. La idea será pasar a los componentes Increment y Decrement la función dispatch por medio de Context de modo que puedan actualizar el valor del contador:

Lo que he hecho ha sido declarar un componente llamado CounterContextProvider el cual emplea el hook useReducer para gestionar el valor del contador y que envuelve a los componentes en su interior con un Context.Provider con dos variables:

  • el valor del contador

De este modo hemos conseguido tener un estado global actualizable y visible desde cualquier componente de la aplicación sin tener que recurrir a la librería Redux.

Vale… ¿y si quiero ejecutar funciones asíncronas?

En mi opinión, el uso de useReducer va fomentar que saquemos las llamadas asíncronas fuera de las acciones y realizar el dispatch de su resultado en vez de recurrir a librerías Redux Thunk o Redux Saga. Esta es mi impresión en todos los (mini)proyectos que he hecho para probar los hooks, ya que parece natural trabajar de esa forma. No obstante, puede que al final aparezca una solución que me convenza de trabajar de otra forma.

Esto no quiera decir que no podamos tener nuestro propio middleware del mismo modo que hace react-redux para llevar a cabo este tipo de acciones. Basta con añadir un código similar al siguiente:

const isPromise = obj => {
return (
!!obj &&
(typeof obj === "object" || typeof obj === "function") &&
typeof obj.then === "function"
);
}
const middleware = dispatch => {
return action => {
if (isPromise(action.payload)) {
action.payload.then(v => {
dispatch({ type: action.type, payload: v });
});
} else {
dispatch(action);
}
};
}

y envolver la función dispatch devuelta por useReducer con la función middleware para poder gestionar llamadas asíncronas.

De este modo, si quisiéramos que el componente Increment actualizase el contador de forma asíncrona bastaría con cambiarlo por:

const Increment = () => {
const { dispatch } = useContext(CounterContext)
const asyncCounterInc = async () => new Promise(resolve => {
setTimeout(() => {
resolve({ value : 1});
}, 1000);
});

return (
<button
onClick={() => dispatch({ type: 'ADD_TO_COUNTER', payload: asyncCounterInc() })}>
Increment
</button>
)
}

y emplear el componente CounterContext del siguiente modo:

<CounterContext.Provider value={{state, dispatch : middleware(dispatch)}}>

es decir, envolviendo la función dispatch con el middleware que nos permite gestionar promesas.

Conclusiones

Como habéis podido ver, los hooks nos proporcionan una alternativa a la librería Redux bastante sencilla y rápida de implementar para proyectos que no requieran gran complejidad.

Por supuesto, es labor vuestra escoger cuando recurrir a esta solución o cuando tirar de soluciones más complejas como puede ser Redux, pero creo que lo interesante es tener cada vez más alternativas.

¿Quieres recibir más artículos como este?

Suscríbete a nuestra newsletter:

Written by

Entre paseo y paseo con Simba desarrollo en Symfony y React

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store