Introducción a Redux.js

Redux es una librería para controlar el estado de nuestras aplicaciones web fácilmente, de una forma consistente entre cliente y servidor, testeable y con una gran experiencia de desarrollo.

Redux está en gran parte influenciado por la arquitectura Flux propuesta por Facebook para las aplicaciones de React.js y por el lenguaje Elm, esta muy pensado para React.js, pero también se puede usar con Angular.js, Backbone.js o simplemente con Vanilla JS.

Principios

Redux se basa en tres principios:

Una sola fuente de la verdad

Todo el estado de tu aplicación esta contenido en un único store

Esto facilita depurar nuestra aplicación y crear aplicaciones universales cuyo estado en el servidor pueda serializarse para luego usarlo en el navegador sin mucho esfuerzo. Otras funcionalidades como atras/adelante se hacen más fáciles de implementar cuando tenemos un solo store con todo el estado de nuestra aplicación.

El estado es de solo lectura

La única forma de modificar el estado es emitir una acción que indique que cambió

Esto te asegura que ninguna parte de la aplicación, como pueden ser eventos de la UI, callbacks o sockets, alteren directamente el estado de tu aplicación, en vez de eso emiten una intención de modificarlo.

Y gracias a que todas las modificaciones se centralizan y se ejecutan una por una es imposible que se pisen cambios. Por último como las acciones son objetos planos pueden mostrarse en consola o almacenarse para volverlas a ejecutar durante el debugging.

Los cambios se hacen mediante funciones puras

Para controlar como el store es modificado por las acciones se usan reducers puros

Los Reducers son funciones puras que reciben el estado actual de la aplicación y la acción a realizar y devuelven un nuevo estado, sin modificar directamente el estado actual. Podemos tener un solo Reducer encargado de toda nuestra aplicación o si esta crece dividirlo en múltiples funciones las cuales podemos controlar en que orden se ejecutan.

Instalando Redux en nuestro proyecto

Para instalar Redux es igual que cualquier librería/framework que esté en npm:

npm i -S redux

Luego podemos importarlo como cualquier otro módulo:

import redux from 'redux';

Internamente va a descargar dos dependencias:

  • loose-envify — Inyecta variables de entorno
  • lodash — Colección de funciones utilitarias para programación funcional

Conceptos básicos

Redux es bastante fácil de aprender, aunque a simple vista no lo parezca, incluso es tan fácil que la librería es increíblemente pequeña (2kb minificada).

Acciones

Las Acciones son POJOs (Plain Old JavaScript Objects) con al menos una propiedad que indica el tipo de acción y, de ser necesario, otras propiedades indicando cualquier otro dato necesario para efectuar nuestra acción. Normalmente se usa el formato definido en el Flux Standard Action (FSA).

{
type: 'ADD_TODO',
payload: {
text: 'Aprender Redux',
},
}

Para enviar una acción a nuestro Store usamos la función store.dispatch() pasando nuestra acción como único parámetro.

Creadores de acciones

Estos son simplemente funciones que pueden o no recibir parámetros y devuelven una acción (un POJO), es muy buena idea, para evitar problemas de consistencia, programar una función por cada tipo de acción y usarlas en vez de armar nuestros objetos a mano.

/**
* Devuelve una acción de tipo ADD_TODO
* @param {String} text Texto del TODO
* @return {Object} Objecto de acción
*/
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: {
text,
},
};
}

Debido a que normalmente son funciones puras son fáciles de testear. Luego de ejecutar nuestra función, para poder despachar la acción, es simplemente llamar a la función dispatch(addTodo(‘Aprender Redux’)).

Reducers

Mientras que las acciones describen que algo ocurrió no especifican como nuestra aplicación reacciona a ese algo. De esto se encargan los Reducers.

Ya que el estado de nuestra aplicación es un único objeto es buena idea empezar a pensar cual va a ser la forma más básica antes de empezar a programar, como ejemplo vamos a suponer que hacemos una aplicación de TODOs por lo que nuestro store va a tener el siguiente formato:

{
todos: [],
}

Ahora que definimos la forma de nuestro store podemos empezar a crear reducers. Un reducer es una función pura que recibe el estado actual y una acción y devuelve el nuevo estado.

(prevState, action) => nextState

Se llaman reducers porque son el tipo de funciones que pasarías a Array.prototype.reduce(reducer[, initialValue]). Es muy importante que se mantengan como funciones puras. Algunas cosas que nunca deberías hacer en un reducer:

  • Modificar sus argumentos directamente (lo correcto es crear una copia)
  • Realizar acciones con efectos secundarios como llamadas a una API o cambiar rutas
  • Ejecutar funciones no puras como Date.now() o Math.random()

Que se mantengan puros quiere decir que pasandole los mismos parámetros debería siempre devolver el mismo resultado. Ahora vamos a programar un reducer para nuestra acción ADD_TODO:

// cargamos el método de Redux para
// poder combinar reducers
import { combineReducers } from 'redux';

function todos(state = [], action) {
switch(action.type) {
case 'ADD_TODO':
// creamos una copia del state actual
const copy = Array.from(state);
// modificamos lo que necesitamos
copy.push(action.payload.text)
// retornamos el nuevo state
return copy;
default:
// si el action.type no existe o no concuerda
// con ningunos de los casos definidos
// devolvemos el estado sin modificar
return state;
}
}

// combinamos nuestros reducers
// los keys que usemos para nuestros reducers
// van a ser usados como keys en nuestro store
// en este ejemplo sería: { todos: [], }
const reducers = combineReducers({
todos,
});

export default reducers;

Como se ve arriba Redux nos provee una función llamada combineReducers() que recibe un objeto con los reducers que definimos y los combina.

El nombre que le pongamos a cada reducer es usado como propiedad del store que creemos y es donde se va a guardar el estado devuelto por el reducer.

Store

Por último necesitamos crear nuestro Store, el store va a tener cuatro responsabilidades:

  1. Almacenar el estado global de la aplicación
  2. Dar acceso al estado mediante store.getState()
  3. Permitir que el estado se actualice mediante store.dispatch()
  4. Registrar listeners mediante store.subscribe(listener)

Es importante recordar que solo podemos tener un store en nuestras aplicaciones de Redux, cuando quieras separar la lógica de manipulación de datos usa la composición de reducers en vez de muchos stores.

Para crear un store necesitamos una función de Redux y el reducer (o los reducers combinados) que vamos a usar:

// cargamos la función para crear un store
import { createStore } from 'redux';
// cargamos nuestros reducers (ya combinados)
import reducers from './reducers.js';
// definimos el estado inicial
const initialState = {
todos: [],
};
// creamos el store
const store = createStore(reducers, initialState);
export default store;

La función createStore simplemente recibe nuestros reducers como primer parámetro y nuestro estado inicial como segundo (y opcional), en el estado inicial podemos desde enviar simplemente la forma básica de nuestro store hasta enviar los datos recibidos desde el servidor.

Obteniendo nuestro estado

Una vez tenemos nuestro store creado podemos acceder al estado que almacena con store.getState() desde cualquier parte de nuestra aplicación donde importemos el store.

import store from './store.js';
// vemos el estado actual del store
console.log(store.getState());

Subscribirse a los cambios de estado

Podemos suscribirnos al store para enterarnos cuando cambia y poder modificar nuestra aplicación en consecuencia usando store.subscribe(callback).

import store from './store.js';
// nos suscribimos al store, esto nos devuelve
// una función que nos sirve para desuscribirnos
const unsubscribe = store.subscribe(() => {
// vemos el nuevo store
console.log(store.getState());
// nos desuscribimos
unsubscribe();
});

Conclusión

Como se puede ver Redux es bastante simple de empezar a usar, y gracias a que es tan simple es posible combinarlo con prácticamente cualquier framework o librería que exista, ya sea React, Backbone, Angular 1/2, Ember, etc.