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 😉
Muy buen artículo y clara la explicación
Muy buen articulo, la explicación del flujo de trabajo es simplemente sencilla y muy comprensible
me ha ayudado bastante a entender mejor redux
Como no… otro post muy interesante Enrique! Pero la verdad es que me he quedado con ganas de saber más. Si en proyecto se decide implementar Redux, cierta lógica de los servicios pasarían a ser gestionada por redux?
saludos
no entiendo nada
segui sin entender con el video, o no me corre nada
No hay ningún vídeo, es un editor online con código donde puedes ejecutar los resultados para visualizarlo.
Básicamente Redux es una forma distinta a la habitual para centralizar el estado. En el ejemplo, el estado (tanto el valor del contador como las veces que se ha clickado cada botón) no está almacenado en ninguna variable que hayas definido tu, sino en la «store» de Redux. Cuando tienes muchos más datos a guardar, es muy cómodo tenerlo todo centralizado (está centralizado, pero puedes acceder de forma independiente). Además, evita que puedas manipular el estado donde te de la gana, porque fuerza el flujo de datos unidireccional.
Una pregunta: Solo hay un solo store y un solo reduce para toda la app o por cada componente hay uno?
Buen articulo, muy bien explicado!
Muy buen artículo. muy claro porque desarrollo con Angular y quería saber de que se trata redux ya que por el tamaño de aplicaciones que me ha tocado desarrollar, con los services e inyección de dependencias mas el local storage es suficiente
Mas claro agua, muy bien redactado, gracias por compartirlo!!!
Sinceramente me ha gustado el artículo. Lo bueno y breve dos veces bueno y encima si hay un código que funciona mucho mejor aún. Bien podría ser éste un punto de partida para poder llegar mediante el uso de VanillaJS a toda la gente interesada en profundizar en Redux independientemente del framework que se quiera posteriormente utilizar.
Lejos, lo mas claro que encontré sobre Redux. Muchas gracias por tu aporte.
Encantado de ayudar 😉
Hola, excelente artículo, muy claro y conciso. Las imágenes no se ven, aparece una imagen de un circulo relleno gris con un signo menos (-) blanco.
Excelente aporte Enrique!! Muchas gracias!!
Hoy empecé a aprender react+redux y está explicación me ayudó mucho. Muchas gracias!
Totalmente despegada tu explicacion, ni en videos encontre algo tan sencillo y bien explicado. Estan sobrevalorados los videos y subvalorados los documentos tecnicos, y la gente no se da cuenta que el problema no es el medio, sino la calidad del material.
Te felicito, lo tomo prestado para enseñarles a unos muchachos de la escuela tecnica (siempre con referencia).
Saludos desde Uruguay.