Redux es una herramienta para la gestión de estado en apps Javascript que nació en 2015 de la mano de @dan_abramov. Aunque suele asociarse a React, lo cierto es que es una librería framework agnostic, que vale la pena conocer aunque no vayas a trabajar con React.
¿Qué es la gestión de estado (state management)?
En programación, se podría definir «estado» como el conjunto de todos los valores almacenados por la aplicación mediante propiedades o variables en cualquier momento de ejecución.
En frontend, el estado puede incluir las respuestas del servidor y la información cacheada, así como datos generados directamente en local que no se han guardado en servidor. A eso hay que añadirle el estado de la interfaz: rutas activas, tabs seleccionados, spinners, controles de paginación…
La gestión de estado consiste en asegurar que la UI muestre correctamente el estado actual de la aplicación y es un pilar fundamental en frontend.
Las aplicaciones sencillas no necesitan darle demasiada importancia a la gestión del estado. Su estado se puede almacenar directamente en las propiedades de los componentes, por ejemplo.
El problema de la gestión de estados aparece cuando la aplicación comienza a crecer. Especialmente cuando los valores de un componente pueden afectar a valores de otros componentes: Puedes entrar en un ciclo de actualizaciones de estado (e interfaz) que hace difícil seguir el hilo de por qué has llegado a un estado concreto.
¿Que aporta Redux a la gestión del estado?
En el frontend moderno, tienes que juntar la continua variación de datos (mutabilidad), con la incertidumbre de cuando se producen (asincronismo), y eso es una combinación peligrosa.
Para apps pequeñas no es un problema, todos los frameworks modernos (Angular, React, Vue) proporcionan sus propios mecanismos para almacenar el estado, al margen de Redux.
¿Cuando me interesa Redux entonces? Pues una señal clara de que necesitas ayuda con la gestión de estados es cuando tienes una app tan grande que en un momento dado pierdes el control del cuándo, cómo y por qué de tu estado.
El propósito de Redux es hacer predecibles los cambios de estado, imponiendo ciertas restricciones sobre como y cuando pueden producirse las actualizaciones. Redux consigue que tu gestión de estado sea transparente y determinista, lo que entre otras cosas aporta…
- Mejor comprensión de la evolución del estado en un momento dado
- Facilidad para incorporar nuevas características a la app
- Un nuevo abanico de herramientas de debugging (como el time travelling)
- Capacidad de reproducir un bug
- Mejoras en el proceso de desarrollo, pudiendo reiniciar la ejecución a partir de un estado concreto.
¿Tiene sentido usar Redux con Angular?
A nivel organizativo, Angular tiene bastante bien solucionada la gestión de estados, pero donde realmente puede ayudar Redux es en el proceso de desarrollo y debuging.
Déjame aclarar algunas diferencias de la gestión de estado original de Angular frente a la de React.
Si prescindes de Redux, en React tienes componentes con estado, que para comunicarlo, pasan el valor de padres a hijos a través de sus propiedades. Eso significa que si tienes un árbol muy grande de componentes, para pasar el estado del componente superior al componente inferior tienes que irlo pasando componente a componente por todos los nodos intermedios. Y eso no mola nada.
En Angular, la forma habitual de almacenar el estado es a través de servicios, que son objetos singleton a los que puede acceder cualquier componente mediante la inyección de dependencias. Volviendo al ejemplo, para pasar el estado del componente superior al inferior en Angular, solo necesitas que ambos inyecten el servicio que contiene dicho estado.
Esta diferencia hace que en Angular no sea tan necesario el uso de Redux hasta que la aplicación no se vuelve realmente grande.
Como funciona Redux
Antes de entrar al detalle, déjame hacer hincapié en los 3 principios de Redux que lo convierten en un contenedor predecible de estados.
Los principios fundamentales de Redux
- Fuente única de verdad: En Redux hay un único objeto que almacena el estado de toda la aplicación. Esto ayuda a la hora de trabajar con apps universales, así como a la hora de debugar y de reiniciar el desarrollo en un punto concreto de ejecución.
-
Inmutabilidad, el estado es read-only. Ninguna interacción puede cambiarlo directamente. Lo único que puedes hacer para conseguirlo es emitir una acción que expresa su intención de cambiarlo.
-
Funciones puras: Usa funciones puras (a mismos inputs, mismos outputs) para definir como cambia el estado en base a una acción. En Redux estas funciones se conocen como reducers y al ser puras, su comportamiento es predecible.
Flujo de actualizaciones en Redux
Aquí tienes un diagrama con el flujo que sigue Redux desde una interacción, hasta que la aplicación actualiza la UI.
Es decir:
- El componente recibe un evento (click, por ejemplo) y emite una acción.
- Esta acción, se pasa a la store, que es donde se guarda el estado único.
- La store comunica la acción junto con el estado actual a los reducers.
- Los reducers, devuelven un nuevo estado, probablemente modificado en base a la acción.
- Los componentes reciben el nuevo estado de la store.
A continuación puedes ver una animación con un ejemplo más detallado, de la presentación de @JenyaTerpil durante la Front End Developer Conference de 2016.
El diagrama anterior tiene buena pinta. Sabes que a misma dupla (action, state0), vas a tener el mismo resultado state1.
Este flujo es muy apropiado para cambios en local a nivel de interfaz, pero claro, es muy básico. ¿Que pasa cuando necesitamos ejecutar operaciones impuras, como por ejemplo pedir datos a un servidor?
Ahí es donde entran los side effects…
Efectos colaterales (side effects) y middleware
Redux está inspirado por la programación funcional y de entrada no tiene en cuenta los efectos colaterales. Los reducers deben ser siempre funciones puras del estilo (state, action) => newState
.
Eso sí, Redux permite incorporar funciones de middleware, que entre otras cosas permiten interceptar las acciones y añadir nuevo comportamiento a las mismas, incluidos los temidos side effects.
A continuación tienes una animación similar a la anterior, donde se incluye un middleware que permite continuar a la acción inicial, mientras que en paralelo se hace una petición a la API, que a la postre lanzará una nueva acción.
Elementos clave de Redux
Todo esto es muy teórico y seguramente quieres ver que aspecto tiene esto de Redux a la hora de la verdad. Vamos por partes…
Store
El store es el centro neurálgico de Redux. Todo en Redux gira a su alrededor y es un objeto único. Una app no puede tener varios stores.
El store es la conexión entre los acontecimientos que pretenden cambiar el estado (acciones) y la lógica que indica como cambiarlo (reducers).
El store:
- Almacena el estado de toda la aplicación
- Da acceso al estado (solo lectura) con
getState()
- Permite lanzar acciones con el método
dispatch(action)
para que las reciban los reducers.
Para inicializar el store debes usar el método createStore()
de Redux, y pasarle tus reducers. Opcionalmente le puedes pasar un segundo argumento con el estado inicial (para debugar o rehidratar en cliente si usas SSR).
// creating a store
import { createStore } from 'redux';
import myReducers from './reducers';
let store = createStore(myReducers);
Acciones
Las acciones son el único mecanismo en Redux para enviar información a tu store.
Son objetos simples que incluyen una propiedad type
y pueden incluir otros campos con información adicional. Muchas veces y por convención, a esa información adicional se le llama payload
.
// simple action to increase counter
const INCREASE_COUNTER = 'INCREASE_COUNTER';
const exampleAction = {
type: INCREASE_COUNTER,
payload: 1
}
Action creators
Normalmente la información adicional no es estática (como en exampleAction
). Lo habitual es crear las acciones mediante funciones, como en el siguiente ejemplo:
// action creator to increase counter in a variable amount
function increaseCounter(amount){
return {
type: INCREASE_COUNTER,
payload: amount
}
}
A partir de aquí, cuando quieres lanzar la acción (por ejemplo al hacer click en un botón), usarías el método store.dispatch
.
Un ejemplo de como lanzar una acción en JS plano sería este:
//how to dispatch an action
//...declare store
import { loadUser } from './actions';
//get reference to the "increaseCounter" button
const increaseCounterButton = document.getElementById('increaseCounterButton');
//on "increaseCounter" button click, dispatch action
increaseCounterButton.addEventListener('click', ()=>{
store.dispatch(increaseCounter(1));
});
Reducers
Los reducers son funciones que definen cómo debe cambiar el estado de la aplicación en respuesta a las acciones.
Como te comentaba, la estructura de los reducers es esta: (state, action) => newState
Aquí tienes un ejemplo muy sencillo:
function counterReducer(state={count:0}, action){
switch(action.type){
case INCREASE_COUNTER:
return {
...state,
count: state.count + action.payload
};
case DECREASE_COUNTER:
return {
...state,
count: state.count - action.payload
};
default:
return state;
}
}
Como puntos a destacar, es interesante mencionar que los reducers deben devolver siempre un estado, aunque sea el original (si no hay cambios). De ahí que use un estado por defecto tanto en el constructor como en el switch.
Middleware
El middleware en React proporciona un punto de extensión para terceros entre el envío de una acción y el momento en que alcanza el reducer.
¿Que cosas se pueden hacer metiendo mano ahí en medio? Pues registrar eventos, generar informes de fallos, realizar llamadas a una API asíncrona, enrutamiento y básicamente lo que se te ocurra.
Crear un middleware desde cero es un paso más avanzado y no te lo detallaré ahora, pero ya te avanzo que pocas veces tienes que hacerlo de cero: Hay cantidad de proyectos open source que proporcionan middlewares para todo lo que imagines en Redux.
Redux en acción
Has visto los elementos básicos de lo que sería un contador cuyo estado se gestiona íntegramente a través de Redux.
Te pongo el ejemplo completo, en vanilla JS para que puedas ver como funciona Redux si ningún framework. En el ejemplo he ampliado la idea para que el estado incluya también el número de clicks que reciben los botones increaseCounterButton
y decreaseCounterButton
por separado.
El resultado es el Codepen que puedes ver a continuación.
Reflexiones personales
Este ejemplo es extremadamente sencillo, pero espero que te sirva para hacerte a la idea de qué es Redux y qué puede aportarle a tu proyecto.
Como he dicho al principio, puede ser algo exagerado para un proyecto muy pequeño, pero cuando trabajas con proyectos grandes es una herramienta muy interesante que te ayuda a definir claramente como afectan las modificaciones de estado a tu interfaz y que además te puede ayudar enormemente al desarrollo y en tareas de debugging.
¿Te ha gustado este artículo? No te cortes, déjame un comentario y ayúdame a compartirlo 😉