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

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
- Creadores de acciones
- Reducers
- Y todo el proceso de conectar React con Redux mediante la librería
react-redux
.
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.

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 adispatch
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 - Sendos componentes
Increment
yDecrement
para actualizar el valor del contador - Un componente
App
para pintar todos ellos y gestionar el estado del contador mediante el hookuseState
.
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
- el segundo la función
dispatch
que podemos emplear para actualizar ese 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
- la función
dispatch
, a la cual pueden acceder los componentesIncrement
yDecrement
para actualizar 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: