Desarrollo de Aplicaciones Web con React.js y Redux.js
Desarrollo de Aplicaciones Web con React.js y Redux.js
Sergio Daniel Xalambrí
Buy on Leanpub

Tabla de contenidos

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:

1 npm i -S redux

Luego podemos importarlo como cualquier otro módulo:

1 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).

1 {
2   "type": "ADD_TODO",
3   "payload": {
4     "text": "Aprender Redux"
5   }
6 }

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.

 1 /**
 2  * Devuelve una acción de tipo ADD_TODO
 3  * @param {String} text Texto del TODO
 4  * @return {Object} Objecto de acción
 5  */
 6 function addTodo(text) {
 7   return {
 8     type: "ADD_TODO",
 9     payload: {
10       text
11     }
12   };
13 }

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:

1 {
2   "todos": []
3 }

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.

1 (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:

 1 // cargamos el método de Redux para
 2 // poder combinar reducers
 3 import { combineReducers } from "redux";
 4 
 5 function todos(state = [], action = {}) {
 6   switch (action.type) {
 7     case "ADD_TODO":
 8       // creamos una copia del estado actual
 9       const copy = Array.from(state);
10       // modificamos lo que necesitamos
11       copy.push(action.payload.text);
12       // retornamos el nuevo estado
13       return copy;
14     default:
15       // si el action.type no existe o no concuerda
16       // con ningunos de los casos definidos
17       // devolvemos el estado sin modificar
18       return state;
19   }
20 }
21 
22 // combinamos nuestros reducers
23 // los keys que usemos para nuestros reducers
24 // van a ser usados como keys en nuestro store
25 // en este ejemplo sería: { todos: [], }
26 const reducers = combineReducers({
27   todos
28 });
29 
30 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:

 1 // cargamos la función para crear un store
 2 import { createStore } from "redux";
 3 // cargamos nuestros reducers (ya combinados)
 4 import reducers from "./reducers.js";
 5 // definimos el estado inicial
 6 const initialState = {
 7   todos: []
 8 };
 9 // creamos el store
10 const store = createStore(reducers, initialState);
11 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.

1 import store from "./store.js";
2 // vemos el estado actual del store
3 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).

1 import store from "./store.js";
2 // nos suscribimos al store, esto nos devuelve
3 // una función que nos sirve para desuscribirnos
4 const unsubscribe = store.subscribe(() => {
5   // vemos el nuevo store
6   console.log(store.getState());
7   // nos desuscribimos
8   unsubscribe();
9 });

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.

Combinando React.js y Redux.js

En el capítulo anterior vimos como funciona Redux.js y dijimos que era posible usarlo con cualquier framework o librería de JavaScript.

Y, aunque esto es cierto, Redux es especialmente bueno al usarlo con librerías como React.js, ya que podés describir tu UI como funciones puras y usar Redux para tener todo el estado de nuestra aplicación y pasarlo a nuestras vistas.

Instalando react-redux

La conexión de React con Redux no esta incluida directamente en Redux, para esto necesitamos bajar react-redux, así que vamos a descargar lo necesario:

1 npm i -S react react-dom react-redux redux

Encapsulando la aplicación

Lo primero que necesitamos es encapsular nuestra aplicación con el componente Provider que trae react-redux. Este componente recibe un único parámetro llamado store el cual es, como su nombre indica, la instancia del Store que usamos.

 1 import React from "react";
 2 import { render } from "react-dom";
 3 import { Provider } from "react-redux";
 4 
 5 import store from "./store";
 6 import App from "./components/App";
 7 
 8 render(
 9   <Provider store={store}>
10     <App />
11   </Provider>,
12   document.getElementById("app")
13 );

Este componente Provider define en el contexto global de React nuestra instancia del store.

Accediendo al Store

Una vez encapsulada nuestra aplicación de React nos toca definir que componentes van a acceder a nuestro Store, ya que no todos lo van a necesitar.

Para hacer eso necesitamos conectar nuestros componentes a Redux, esto se logra con un decorador que trae react-redux llamado connect.

 1 // importamos el decorador @connect de react-redux
 2 import { connect } from "react-redux";
 3 import React from "react";
 4 import UserItem from "./UserItem";
 5 
 6 // aplicamos el decorador @connect a nuestro componente
 7 @connect()
 8 class UserList extends React.Component {
 9   render() {
10     // renderizamos el listado de usuarios que
11     // recibimos como props del Store
12     return (
13       <section>
14         {this.props.users.map(user => <UserItem {...user} key={user.id} />)}
15       </section>
16     );
17   }
18 }
19 export default UserList;

De esta forma nuestro componente UserList va a tener dentro de sus props todos los datos del Store. Con esto ya podemos renderizar nuestra aplicación usando los datos almacenados en el Store de Redux.

Optimizando

Aunque el método anterior sea más que suficiente no es lo mejor a nivel de performance, ya que de esta forma cada vez que cambie algo del Store se va a volver a renderizar UserList, incluso si la lista de usuario no cambio.

Para mejorar esto el decorador connect puede recibir una función que define que datos pasar al componente conectado.

 1 function mapStateToProps(state, props) {
 2   // armamos un objeto solo con los
 3   // datos del store que nos interesan
 4   // y lo devolvemos
 5   return {
 6     users: state.users,
 7   };
 8 }
 9 
10 // aplicamos el decorador @connect pasándole
11 // nuestra función mapStateToProps
12 @connect(mapStateToProps)
13 class UserList extends React.Component {
14   ...
15 }

De esta forma podemos solo enviar a UserList el listado de usuarios, así cuando se modifique otra cosa que no sea la lista de usuarios no se va a volver a renderizar el componente.

Despachando acciones

Entre las props que el decorador connect inyecta a nuestro componente se encuentra la función dispatch del Store, con la cual podemos despachar acciones.

 1 // cargamos nuestro creador de acciones
 2 import sendData from '../actions/send-data';
 3 
 4 @connect()
 5 class UserList extends React.Component {
 6   handleSendData() {
 7     const action = sendData();
 8     // despachamos la acción al store
 9     this.props.dispatch(action);
10   }
11   ...
12 }

Resulta que connect como segundo argumento podemos pasarle una función que nos permite controlar la función dispatch para mandar una personalizada.

 1 import sendData from '../actions/send-data';
 2 // importamos el método bindActionCreators de Redux
 3 import { bindActionCreators } from 'redux';
 4 
 5 function mapStateToProps(state, props) { ... }
 6 
 7 function mapDispatchToProps(dispatch, props) {
 8   // creamos un objeto con un método para crear
 9   // y despachar acciones fácilmente y en
10   // una sola línea
11   const actions = {
12     sendData: bindActionCreators(sendData, dispatch),
13   };
14 
15   // devolvemos nuestras funciones dispatch
16   // y los props normales del componente
17   return { actions };
18 }
19 
20 // decoramos nuestro componente pasándole las
21 // funciones mapStateToProps y mapDispatchToProps
22 @connect(mapStateToProps, mapDispatchToProps)
23 class UserList extends React.Component {
24   handleSendData() {
25     // creamos y despachamos la acción sendData
26     this.props.actions.sendData();
27   }
28   ...
29 }

De esta forma podemos mandar a nuestro componente las acciones que necesitamos y que con solo ejecutarlas ya haga el dispatch de estas.

Funcionamiento sin decoradores

Aunque connect esta pensado como decorador es posible usarlo como una función normal sin necesidad de usar Babel con el plugin babel-plugin-transform-decorators-legacy para soportar decoradores de la siguiente forma.

1 export default connect(mapStateToProps, mapDispatchToProps)(Component);

Como siempre connect recibe mapStateToProps y mapDispatchToProps como parámetros, solo que además devuelve una función que recibe el componente a conectar y nos devuelve el componente conectado, el cual simplemente exportamos y listo, conseguimos el mismo resultado que usándolo como un decorador.

Esto es util si no queremos usar decoradores todavía, ya que la actual propuesta es muy posible que cambie a futuro.

Conectando componentes puros

Aunque lo normal es usar connect con componentes hechos con clases es completamente posible usarlo con componentes puros si hacemos uso del decorador como una función.

1 import React from 'react';
2 import { connect } from 'react-redux';
3 function mapStateToProps(state, props) { ... }
4 // el componente puro a conectar
5 function UserItem(props) { ... }
6 // exportamos el componente conectándolo gracias a connect
7 export default connect(mapStateToProps)(UserItem);

Así podríamos crear toda nuestra aplicación solamente con componentes puros, sin necesidad de usar clases. Esto nos obligaría, de verdad, a mantener el estado de toda nuestra aplicación en Redux y dejar React para la UI.

Conclusión

Integrar React y Redux es bastante simple y gracias a connect es muy fácil controlar qué datos del Store le llegan a cada componente permitiendo una mejor performance y evitando renders innecesarios.

Middlewares en Redux.js

Luego de ver como funciona Redux.js y como usarlo con React.js, vamos a aprender a crear middlewares, estos son funciones de orden superior que nos permiten agregar funcionalidad a los Stores de Redux.

Un middleware sirve para muchas tareas diferentes, registrar datos, capturar errores, despachar promesas, cambiar rutas, etc. básicamente cualquier tarea con efectos secundarios se puede hacer en un middleware.

API

Como dije un middleware es una función, esta función va a recibir como parámetro una versión reducida del Store, con solamente los métodos dispatch y getState.

Esta función debe devolver otra función que va a recibir una función normalmente llamada next que nos sirve para llamar al siguiente middleware.

Por último devolvemos una nueva función que va a recibir la acción que se esta ejecutando. Para entenderlo bien veámoslo en código.

 1 // nuestro middleware
 2 function middleware(store) {
 3   // recibimos { dispatch, getState }
 4   return function wrapDispatch(next) {
 5     // recibimos next
 6     return function dispatchAndDoSomething(action) {
 7       // recibimos la acción despachada
 8       // acá va nuestro código
 9     };
10   };
11 }

Esta es básicamente la estructura de un middleware, dentro de nuestra función dispatchAndDoSomething es donde vamos a colocar todo nuestro código. Otra forma escribir este código es usando arrow functions para que quede más simple y conciso.

 1 // esto:
 2 const middleware = store => next => action => {
 3   // acá va nuestro código
 4 };
 5 // es lo mismo que esto:
 6 const middleware = function(store) {
 7   return function(next) {
 8     return function(action) {
 9       // acá va nuestro código
10     };
11   };
12 };

De esta forma nos quedan todos los argumentos en una línea y simplemente escribimos el código necesario.

Middleware de logging

Supongamos que queremos crear un middleware que muestre en consola cada acción despachada, el cambio en el Store y cuanto tarda en ejecutase.

 1 const logger = store => next => action => {
 2   // agrupamos lo que vamos a mostrar en
 3   // consola usando el tipo de la acción
 4   console.group(action.type);
 5   // mostramos el estado actual del store
 6   console.debug("current state", store.getState());
 7 
 8   // mostramos la acción despachada
 9   console.debug("action", action);
10 
11   // empezamos a contar cuanto se tarda en
12   // aplicar la acción
13   console.time("duration");
14 
15   // pasamos la acción al store
16   next(action);
17 
18   // terminamos de contar
19   console.timeEnd("duration");
20 
21   // mostramos el estado nuevo
22   console.debug("new state", store.getState());
23   // terminamos el grupo
24   console.groupEnd();
25 };

Este simple middleware registra en consola el estado del Store, la acción despachada, el nuevo estado y cuanto se tarda en realizar los cambios (perfecto para identificar problemas de performance).

Middleware de errores

Creemos otro middleware de ejemplo, hagamos uno que en caso de un error nos muestre que ocurrió.

 1 const catchError = store => next => action => {
 2   try {
 3     // aplicamos la acción
 4     next(action);
 5   } catch (error) {
 6     // mandamos nuestro error a algún servicio
 7     // como Sentry o Track:js donde luego
 8     // podamos revisarlo con detalle
 9     errorLogger.send(error);
10   }
11 };

Este middleware super simple nos permitiría capturar cualquier error que ocurra y registrarlo ya sea en consola o en algún servicio.

Usando un middleware

Luego de creado nuestro middleware para empezar a usarlo tenemos que aplicarlo al Store al momento de crearlo.

1 import { applyMiddleware, createStore } from "redux";
2 import reducer from "./reducer";
3 import logger from "./middlewares/logger";
4 import catchError from "./middlewares/catch-error";
5 const store = createStore(reducer, applyMiddleware(catchError, logger));

De esta forma aplicamos nuestros dos middlewares al momento de crear el Store. Cabe destacar que el orden en que se pasen los middlewares importa en como se van a ejecutar, en nuestro caso primero se ejecuta catchError y luego logger de forma que si tanto logger como los reducers tienen algún error catchError lo va a registrar y si no logger va a mostrar la información en la consola.

Conclusión

Usar middlewares nos permite extender fácilmente la funcionalidad de nuestros Stores de Redux.js, sin escribir mucho código, lo que puede resultarnos muy útil en aplicaciones medianas o grandes para implementar fácilmente features que necesitemos.

Acciones asíncronas en Redux.js

Redux, por su naturaleza puramente funcional, esta pensado para realizar tareas síncronas:

1 (state, action) => newState;

Sin embargo, debido a como funciona JS lo más común es trabajar de forma asíncrona, por ejemplo hacer una petición AJAX a una API es asíncrono y luego de esto seguramente vamos a querer modificar el Store en base a la respuesta.

Para esto se usan las acciones asíncronas, hay varias formas de trabajar con acciones asíncronas en Redux, una es hacerlo a mano, otra opción es usar es usar distintos middlewares.

Manualmente

Para hacerlo manualmente primero necesitamos nuestro creador de acciones, por ejemplo.

1 export default function addTodo(content) {
2   return {
3     type: "ADD_TODO",
4     payload: { content }
5   };
6 }

Luego es bastante simple, al terminar de ejecutar nuestra función asíncrona vamos a despachar nuestra acción normalmente.

 1 import store from "./store";
 2 import addTodo from "./actions/add-todo";
 3 
 4 fetch("/api/todos/1")
 5   .then(response => response.json()) // obtenemos los datos
 6   .then(addTodo) // creamos la acción
 7   .then(store.dispatch) // despachamos la acción
 8   .catch(error => {
 9     // si hay algún error lo mostramos en consola
10     console.error(error);
11   });

Con esto al terminar nuestra petición creamos la acción en base a la respuesta y luego la despachamos, bastante simple. Esto podría ocurrir como resultado de que el usuario envíe un formulario o haga click sobre un botón.

Usando middlewares

Si no queremos que este proceso sea manual para cada función asíncrona, podemos usar middlewares que se encarguen de esto automáticamente, para esto hay varias opciones.

Con redux-thunk y redux-promise

Estos dos middlewares nos permiten hacer un dispatch de una función que devuelva una promesa y que se haga el dispatch de la acción automáticamente.

Primero vamos a bajarlos con npm.

1 npm i -S redux-thunk redux-promise
  • redux-thunk: permite despachar funciones que devuelvan promesas y el Store se encarga de ejecutarlos
  • redux-promise: permite despachar promesas y el Store se encarga de esperar que se completen, las promesas deben devolver una acción

Para que estos funcionen primero necesitamos crear nuestro Store aplicándole los middlewares.

1 import { createStore, applyMiddleware } from "redux";
2 import promise from "redux-promise";
3 import thunk from "redux-thunk";
4 
5 import reducers from "./reducers";
6 
7 export default createStore(reducers, applyMiddleware(thunk, promise));

De esta forma creamos nuestro store con redux-thunk y redux-promise como middlewares. Desde ahora además de despachar objetos (para acciones síncronas), podemos despachar funciones que devuelvan promesas para acciones asíncronas.

 1 import store from "./store";
 2 import addTodo from "./actions/add-todo";
 3 // creamos un pequeño cliente para un api
 4 const api = {
 5   todos: {
 6     retrieve() {
 7       // hacemos el request y nos encargamos de convertir
 8       // la respuesta en una acción válida
 9       return fetch("/api/todo")
10         .then(response => response.json()) // obtenemos los datos
11         .then(addTodo); // los convertimos en una acción
12     }
13   }
14 };
15 
16 store.dispatch(api.todos.retrieve());

Gracias a los middlewares que aplicamos Redux va a esperar que la función api.todos.post se termine de ejecutar y que el resultado sea una acción estándar de Flux (con type y payload) y cuando se complete va a, efectivamente, despachar nuestra acción igual que si hubiésemos hecho todo a mano.

Otras opciones

Además de usar redux-thunk y redux-promise hay otros middlewares que nos pueden servir al momento de trabajar con acciones asíncronas.

Todos estos son buenas opciones, redux-saga en particular esta tomando mucha fuerza, y es apoyado por el mismo creador de Redux.

Conclusión

Como ven, trabajar con acciones asíncronas es en realidad muy fácil, incluso haciéndolo a mano. La gran ventaja de usar middlewares que nos solucionen esto es que si en muchas partes vamos a usar acciones asíncronas (y es muy probable que pase) nos ahorramos mucho trabajo cada vez que despachamos la acción.

Pruebas unitarias en Redux.js

Cuando desarrollamos una aplicación con Redux.js la mayor parte del código que escribas van a ser funciones puras, esto hace que crear pruebas unitarias para nuestra aplicación sea más fácil que nunca.

Preparando el ambiente de pruebas

Lo primero que necesitamos para empezar a hacer pruebas es configurar nuestro ambiente de desarrollo local para correr las pruebas. Para esto vamos a usar las librerías tape y tap-spec.

1 npm i -D tape tap-spec
  • tape: nos sirve para realizar nuestras pruebas
  • tap-spec: formatea y colorea el resultado de las pruebas en consola.

Para usarlos vamos a crear una carpeta llamada test y dentro un index.js que es nuestro entry point para pruebas. Luego vamos a crear un script en package.json llamado test.

1 {
2   ...
3   "scripts": {
4     "test": "tape -r babel-core/register test/index.js | tap-spec"
5   }
6   ...
7 }

Como se ve vamos a estar usando Babel.js para transpilar el código de nuestras pruebas. Cuando vayamos a correr nuestras pruebas simplemente usamos el comando de npm.

1 npm test

Y con esto ya ejecutamos las pruebas.

Creadores de acciones

Un creador de acciones es simplemente una función pura que recibe ciertos parámetros y devuelve un objeto que describe una acción. Por ejemplo.

1 export default function addTodo(message) {
2   return {
3     type: "ADD_TODO",
4     payload: {
5       message
6     }
7   };
8 }

Ese es un ejemplo de un creador de acciones común. Vamos a crearle una prueba.

 1 import test from "tape";
 2 import addTodo from "../actions/add-todo";
 3 test("ADD_TODO action creator", t => {
 4   t.plan(1);
 5   t.deepEquals(
 6     addTodo("hello world"),
 7     {
 8       type: "ADD_TODO",
 9       payload: {
10         message: "hello world"
11       }
12     },
13     "it should return the expected object"
14   );
15 });

Con esto ya tenemos una prueba para verificar que nuestro creador de acciones funciona correctamente. En general todos los creadores de acciones van a funcionar de esta forma así que probarlos es bastante sencillo.

Reducers

Los Reducers son la parte más importante de cada aplicación de Redux, y deberían todos tener pruebas unitarias para asegurarse su correcto funcionamiento. Un ejemplo simple de un reducer puede ser el siguiente.

 1 // definimos el estado inicial
 2 const initialState = [];
 3 
 4 function todos(state = initialState, action = {}) {
 5   switch (action.type) {
 6     // si la acción es ADD_TODO
 7     case "ADD_TODO":
 8       // copiamos el estado actual
 9       const newState = Array.from(state);
10       // agregamos el mensaje nuevo
11       newState.push(action.payload.message);
12       // devolvemos el nuevo estado
13       return newState;
14     // si no identificamos la acción
15     default:
16       // devolvemos el estado sin tocarlo
17       return state;
18   }
19 }
20 
21 export default todos;

Para poder hacer pruebas sobre un reducer necesitamos simplemente ejecutarlo pasándole un estado y una acción y ver el resultado que devuelve.

 1 import test from "tape";
 2 import addTodo from "../actions/add-todo";
 3 import todos from "../reducers/todos";
 4 
 5 test("todos reducer", t => {
 6   t.plan(2);
 7   const defaultState = todos();
 8   t.deepEquals(defaultState, [], "it should return an empty array as default");
 9   const message = "Hello world";
10   const state = todos(defaultState, addTodo(message));
11   t.deepEquals(state, [message], "it should return an array with the message");
12 });

Con esto ya probamos que ocurre cuando no recibe una acción y que ocurre cuando recibe una acción de tipo ADD_TODO. Gracias a estas dos simples pruebas podemos asegurarnos de que nuestro reducer funciones correctamente.

Middlewares

Acá ya se puede volver más complicado de probar, principalmente porque depende del middleware que estemos probando el como vamos a escribir nuestras pruebas. Para este ejemplo vamos a usar el middleware socket.io-redux. Primero veamos el código del middleware.

1 const socketIO = socket => () => next => action => {
2   if (action.meta && action.meta.socket && action.meta.socket.channel) {
3     socket.emit(action.meta.socket.channel, action);
4   }
5   return next(action);
6 };
7 export default socketIO;

Ese es el código de nuestro middleware, básicamente recibe una acción y valida que tenga la propiedad meta, que esta sea un objeto con una propiedad socket que a su vez sea otro objeto con la propiedad channel. Si posee todo esto entonces emite la acción a través del canal especificado en la metadata de la acción. Por último pasa la acción al siguiente middleware o al reducer.

Ahora veamos como hacer una prueba de este middleware.

 1 import test from 'tape';
 2 import socketIO from 'socket.io-redux';
 3 // simulamos la función next que va a usar el middleware
 4 function next(action) { return action; }
 5 test('socket.io middleware', t => {
 6   t.plan(3);
 7   // nuestra acción de prueba con los datos necesarios
 8   const testAction = {
 9     type: 'ADD_NEW',
10     payload: 'hello world!',
11     meta: { socket: { channel: 'add:new' } },
12   };
13   // simulamos el objeto socket con el método emit
14   const socket = {
15     emit(channel, data) {
16       // nuestro falso método emit
17       // acá hacemos las pruebas
18       t.equals(
19         channel,
20         'add:new',
21         'it should have the channel "add:new"'
22       );
23       t.deepEquals(
24         data,
25         testAction,
26         'it should have the action as data'
27       );
28     },
29   };
30   // ejecutamos el middleware y guardamos el resultado
31   const action = socketIO(socket)()(next)(testAction);
32   t.deepEquals(
33     action,
34     testAction,
35     'it should return the passed action'
36   );
37 });

Conclusiones

Hacer pruebas unitarias de nuestro código es super importante para evitarnos problemas y encontrar errores más rápido.

En el caso de aplicaciones de Redux ya que la mayor parte de nuestro código no depende directamente de Redux para funcionar es muy simple hacer pruebas unitarias en este por lo que hay no excusa para no hacer pruebas y ser mejores desarrolladores.

Estructura de archivos Ducks para Redux.js

Al realizar una aplicación con Redux es muy común manejar la siguiente estructura de archivos:

1 |_ /actions   # Los creadores de acciones
2 |_ /constants # Las constantes, como los tipos de acciones
3 |_ /reducers  # Los reducers de la aplicación

Aunque esta forma funciona, con el tiempo uno se encuentra casos donde un reducer tiene un solo tipo de acción posible y por lo tanto un solo creador de acciones. Y sin embargo terminamos creando al menos tres archivos para eso (aunque los tipos de acciones se pueden guardar todos juntos).

Para solucionar eso existe Ducks.

Que es

Ducks es una forma de modularizar partes de una aplicación de Redux juntando reducers, tipos de acciones y creadores de acciones juntos de una forma fácil de entender y portar.

El nombre del formato (ducks) viene de la pronunciación de la última sílaba de Redux en inglés.

Como funciona

Veamos un ejemplo simple en código primero.

 1 // tipo de acción
 2 const CHANGE_FILTER = "my-app/filters/CHANGE_FILTER";
 3 // nuestro reducer
 4 export default function reducer(state = "", action = {}) {
 5   switch (action.type) {
 6     case CHANGE_FILTER:
 7       return action.payload;
 8     default:
 9       return state;
10   }
11 }
12 // creador de acciones
13 export function changeFilter(filter) {
14   return { type: CHANGE_FILTER, payload: filter };
15 }

Un módulo de Ducks debe seguir ciertas reglas (que se ven reflejadas en el código anterior).

Reglas

Un módulo…

  1. DEBE exportar por defecto una función llamada reducer().
  2. DEBE exportar sus creadores de acciones como funciones.
  3. DEBE definir sus tipos de acciones en el formato modulo-app/reducer/ACTION_TYPE.
  4. PUEDE exportar sus tipos de acciones como UPPER_SNAKE_CASE si otro reducer la va a usar o si esta publicada como una librería reusable.

Como usarlo

Para usarlo simplemente importas el duck en tu listado de reducers de la siguiente forma.

1 import { combineReducers } from "redux";
2 // los reducers
3 import filters from "./ducks/filters";
4 export default combineReducers({
5   filters
6 });

También es posible importar los creadores de acciones.

1 import * as filtersActions from "./ducks/filters";
2 // o
3 import { changeFilter } from "./ducks/filters";

Y así vas a importarlos listos para ser usados

Conclusión

Implementar e incluso migrar a esta estructura es muy fácil y ayuda mucho a mejorar nuestra experiencia como desarrolladores al hacer nuestros proyectos son más mantenibles a largo plazo y fácil de entender para nuevos desarrolladores.

Creando código modular con ducks de Redux

En el capítulo anterior hablamos que una buena práctica al momento de ordenar nuestro código de Redux.js es usar el formato de módulos ducks.

Este formato nos dice que nuestros módulos deben tener sus tipos de acciones, sus creadores de acciones y su reducer en un solo archivo, y debe exportar estos últimos dos para que sean usados en nuestra aplicación.

Ahora vamos a ver como podemos crear un módulo usando este formato fácilmente con una librería que nos ahorra mucho trabajo.

Instalando dependencias

Primero vamos a instalar lo que necesitamos:

1 npm i -S redux redux-duck immutable
  • redux-duck: Librería utilitaria para crear ducks fácilmente.
  • immutable: Librería para trabajar con estructuras de datos inmutable.

Creando nuestro duck

Vamos a crear un duck para controlar un listado de mensajes en una aplicación de chat. Para eso vamos a crear un archivo donde vamos a tener nuestro módulo, dentro vamos a colocar el siguiente código.

1 import { createDuck } from "redux-duck";

Primero importamos la función createDuck de redux-duck.

1 const duck = createDuck("messages", "chat");

Luego vamos a crear nuestro duck, el primer parámetro que vamos a pasar es el nombre del mismo, el segundo es el nombre de la aplicación, este parámetro es opcional.

Definiendo tipos de acciones

1 const ADD_MESSAGE = duck.defineType("ADD_MESSAGE");
2 const REMOVE_MESSAGE = duck.defineType("REMOVE_MESSAGE");

Luego vamos a definir los tipos de acciones que vamos a tener en nuestro módulo. El método defineType recibe como único parámetro un string con el nombre de la acción y devuelve un nuevo string con el formato:

1 app - name / duck - name / ACTION_NAME;

Donde el nombre de la aplicación es opcional, como dijimos antes. En nuestro ejemplo quedarían dos strings con este formato:

1 ADD_MESSAGE === "chat/messages/ADD_MESSAGE";
2 REMOVE_MESSAGE === "chat/messages/REMOVE_MESSAGE";

Creando nuestros creadores de acciones

1 export const addMessage = duck.createAction(ADD_MESSAGE);
2 export const removeMessage = duck.createAction(REMOVE_MESSAGE);

Luego vamos a exportar el resultado de ejecutar el método createAction pasándole los tipos de acciones que definimos antes.

Este método nos devuelve una función que crea objetos de acciones con la propiedad type igual al valor que le indicamos al crearla. Esta función puede recibir cualquier valor como parámetro y lo va a definir como payload de la acción devuelta.

Creando nuestra función reductora

 1 const initialState = {
 2   list: Immutable.List(),
 3   data: Immutable.Map()
 4 };
 5 export default duck.createReducer(
 6   {
 7     [ADD_MESSAGE]: (state, { payload }) => {
 8       return {
 9         list: state.list.push(payload.id + ""),
10         data: state.data.set(payload.id + "", payload)
11       };
12     },
13     [REMOVE_MESSAGE]: (state, { payload }) => {
14       return {
15         list: state.list.filterNot(id => id === payload.id),
16         data: state.data
17       };
18     }
19   },
20   initialState
21 );

Por último creamos y hacemos un export default del valor devuelto por el método createReducer.

Este recibe dos parámetros, el primero es un objeto cuyos nombres de propiedades sean los strings creados anteriormente al definir los tipos de acciones y los valores sean funciones que reciben el estado y la acción.

Y como segundo parámetro recibe el estado inicial de nuestro reducer, este puede ser un string o un objeto. Esta función nos devuelve nuestra función reductora (reducer) que luego exportamos para que se pueda usar.

Código final

 1 // importamos createDuck de redux-duck
 2 import { createDuck } from "redux-duck";
 3 // importamos Immutable para usarlos luego en nuestro estado inicial
 4 import Immutable from "immutable";
 5 // creamos nuestro duck
 6 const duck = createDuck("messages", "chat");
 7 // definimos los tipos de acciones
 8 const ADD_MESSAGE = duck.defineType("ADD_MESSAGE");
 9 const REMOVE_MESSAGE = duck.defineType("REMOVE_MESSAGE");
10 // creamos nuestros creadores de acciones
11 export const addMessage = duck.createAction(ADD_MESSAGE);
12 export const removeMessage = duck.createAction(REMOVE_MESSAGE);
13 // definimos el estado inicial de nuestro
14 const initialState = {
15   list: Immutable.List(),
16   data: Immutable.Map()
17 };
18 // creamos nuestra función reductora (reducer)
19 export default duck.createReducer(
20   {
21     [ADD_MESSAGE]: (state, { payload }) => {
22       return {
23         list: state.list.push(payload.id + ""),
24         data: state.data.set(payload.id + "", payload)
25       };
26     },
27     [REMOVE_MESSAGE]: (state, { payload }) => {
28       return {
29         list: state.list.filterNot(id => id === payload.id),
30         data: state.data
31       };
32     }
33   },
34   initialState
35 );

Conclusión

Modularizar nuestro código en ducks nos ayuda tener código más fácil de mantener, probar y reutilizar, y la librería redux-duck nos facilita crear estos módulos de una forma sencilla y organizada.

Manejo de errores en Redux.js

¿Que ocurre si una acción llega con un dato mal formado? ¿Si a un reducer le falta un punto y coma? Cuando trabajamos con código no hay forma de evitar al 100% los errores. Por esa razón es muy importante capturarlos para que no rompan nuestra aplicación y enterarnos si pasó algo.

En Redux nuestro código donde es muy probable que hayan errores son los reducers y los middlewares, si quisiéramos capturar los errores en ambos por separado tendríamos que a cada reducer y a cada middleware envolverlos en un try/catch y manejar sus errors individualmente.

¡Pero para evitar esto podemos usar un middleware!

Creando nuestro middleware

Vamos a ver entonces como crear un middleware que capture nuestros errores:

1 const tryCatch = errorHandler => () => next => action => {
2   try {
3     return next(action);
4   catch (error) {
5     return errorHandler(error);
6   }
7 };

Ese va a ser nuestro middleware. Básicamente lo que hace es intenta llamar al siguiente middleware (o reducer) y si en algún momento hay un error lo captura y ejecuta la función errorHandler que le hayamos pasado al instanciarlo.

Ahora para aplicar nuestro middleware es tan fácil como hacer lo siguiente al crear nuestro Store.

 1 import { createStore, applyMiddleware } from "redux";
 2 // nuestro middleware
 3 import tryCatch from "./middlewares/try-catch";
 4 // otro posible middleware de un tercero
 5 import someMiddlware from "some-middleware";
 6 import reducer from "./reducer";
 7 function errorHandler(error) {
 8   // acá hacemos algo con el error como mostrarlo en consola
 9   // o mejor aún mandarlo a algún sistema como Sentry o track:js
10   // ¡incluso a nuestro propio servicio interno!
11   console.error(error);
12 }
13 export default createStore(
14   reducer,
15   applyMiddleware(tryCatch(errorHandler), someMiddleware)
16 );

Como se puede ver es bastante fácil de capturar nuestros errores con un simple middleware, solo tenemos que asegurarnos de que siempre sea el primer middleware para que pueda atrapar los errores de todo nuestro código.

Usando uno ya hecho

Si no queremos crear nuestro propio middleware para capturar errores podemos simplemente usar uno ya hecho descargándolo de npm:

1 npm i -S redux-catch
  • redux-catch: Middleware para capturar errores
    Ahora solo tendríamos que importar redux-catch en vez de nuestro propio middleware y pasarle nuestra función errorHandler para que sepa que hacer con los errors y ya estamos listo.

Adicionalmente el errorHandler que le pasemos a redux-catch recibe como segundo parámetro el método getState que nos puede servir para saber el estado de la aplicación en el momento del error.

Conclusión

Nuestra aplicación puede tener errores por un montón de razones, ya sean errores de sintaxis, algún bug o que simplemente un dato llegó mal formado y nuestro código no supo que hacer.

Y capturarlos es importante para evitar que nuestra aplicación se rompa y ya no le funcione al usuario e implementar un middleware tan simple nos puede ayudar mucho a dar una mejor experiencia de usuario e incluso a arreglar bugs antes de que el usuario se entere.

Usando Redux en el servidor con Socket.io

Redux fue hecho para controlar el estado de la UI de una aplicación. Resulta que mientras podamos tener una única instancia del store Redux también puede servir en Backend, por ejemplo en aplicaciones Real-time usando Socket.io, donde el estado de la aplicación se mantendría, e incluso compartiría entre varios usuarios conectados.

Instalación de dependencias

1 npm i -S redux socket.io socket.io-client redux-duck
  • socket.io: Librería para trabajar fácilmente con WebSockets en Node.js
  • socket.io-client: Cliente para conectarse a un servidor de WebSockets.
  • redux-duck: Librería para crear ducks de Redux.

Creando nuestro Store y Reducers

Lo primero que vamos a hacer es crear los reducers, imaginemos que tenemos una aplicación de chat, el estado de nuestra aplicación podría ser algo así:

 1 type user = {
 2   id: number,
 3   username: string
 4 };
 5 type message = {
 6   id: number,
 7   author: number,
 8   content: string
 9 };
10 type state = {
11   users: Map<user>,
12   messages: List<message>
13 };

Vamos entonces a crear los ducks de nuestra aplicación.

 1 import { createDuck } from "redux-duck";
 2 import { List as list } from "immutable";
 3 const duck = createDuck("messages", "chat");
 4 const ADD = duck.defineType("ADD");
 5 const REMOVE = duck.defineType("REMOVE");
 6 export const addMessage = duck.createAction(ADD);
 7 export const removeMessage = duck.createAction(REMOVE);
 8 export default duck.createReducer(
 9   {
10     [ADD]: (state, { payload = {} }) => {
11       return state.push(map(payload));
12     },
13     [REMOVE]: (state, { payload = {} }) => {
14       return state.filterNot(message => message.get("id") === payload.id);
15     }
16   },
17   list()
18 );

Ese va a ser nuestro duck para los mensajes del chat.

 1 import { createDuck } from "redux-duck";
 2 import { Map as map } from "immutable";
 3 const duck = createDuck("user", "chat");
 4 const ADD = duck.defineType("ADD");
 5 export const addUser = duck.createAction(ADD);
 6 export default duck.createReducer(
 7   {
 8     [ADD]: (state, { payload = {} }) => {
 9       return state.set(payload.id + "", map(payload));
10     }
11   },
12   map()
13 );

Y ese va a ser nuestro duck para manejar los usuarios. Como vemos nuestros ducks son muy simples, solo podemos agregar usuarios y en cuanto a los mensajes solo podemos agregar y quitar mensajes.

Ahora para crear nuestro reducer nos traemos los de nuestros ducks:

1 import { combineReducers } from "redux";
2 import messages from "./ducks/messages";
3 import users from "./ducks/users";
4 export default combineReducers({
5   messages,
6   users
7 });

Y con eso ya tenemos nuestro reducer listo. Ahora vamos a crear nuestro servidor de WebSockets.

Servidor de WebSockets

Una vez que tenemos nuestro reducer y nuestros creadores de acciones vamos a crear un servidor de WebSockets usando socket.io.

 1 import Server from "socket.io";
 2 export default function startServer(store) {
 3   // creamos el servidor escuchando el puerto que
 4   // recibimos como variable de entorno
 5   const io = new Server().attach(process.env.PORT);
 6   // nos suscribimos a los cambios del store y
 7   // mandamos el estado actual en cada cambio por
 8   // el canal 'state' de socket.io
 9   store.subscribe(() => io.emit("state", JSON.stringify(store.getState())));
10   // cuando el usuario se conecte
11   io.on("connection", socket => {
12     // le emitimos el estado actual
13     socket.emit("state", JSON.stringify(store.getState()));
14     // y escuchamos cada acción que mande
15     socket.on("action", store.dispatch.bind(store));
16   });
17   return io;
18 }

Luego vamos a iniciar nuestro servidor y pasarle nuestro store.

1 import { createStore } from "redux";
2 import reducer from "./reducer";
3 import startServer from "./start-server";
4 const store = createStore(reducer);
5 const server = startServer(store);

Ahora solo nos queda levantar nuestro servidor usando Node.js

1 npm start

Con esto ya tenemos un servidor de WebSockets cuyo estado se guarda en Redux. Ahora vemos como sería un cliente sencillo.

Cliente web

Primero vamos a crear un duck para nuestra aplicación.

1 import { createDuck } from 'redux-duck';
2 const duck = createDuck('client', 'chat');
3 const UPDATE_STATE = duck.defineType('UPDATE_STATE');
4 export const updateState = duck.createAction(UPDATE_STATE);
5 export default duck.createReducer({
6   [UPDATE_STATE]: (state, { payload = {} }) => {
7     return payload;
8   },
9 ], {});

Ese super simple duck va a ser toda nuestra aplicación de Redux en el Frontend. Ahora vamos a iniciar nuestro Store y conectarnos al servidor de sockets.

 1 import io from 'socket.io-client';
 2 import { createStore } from 'redux';
 3 import reducer, { updateState } from './duck/client';
 4 // nos conectamos al servidor
 5 const socket = io('http://localhost');
 6 // al conectarnos
 7 socket.on('connect', initialState => {
 8   // recibimos el estado inicial y creamos el store
 9   const store = createStore(reducer, initialState);
10   // cuando el servidor nos mande una actualización
11   // despachamos la acción updateState
12   socket.on('state', nextState => {
13     store.dispatch(updateState(nextState);
14   });
15 });

Con eso ahora el estado de nuestra aplicación nos va a llegar por WebSockets y con eso vamos a iniciar nuestro Store, además en cada cambio que se realice en el servidor vamos a recibir todo el nuevo estado y vamos a actualizar el Store.

Por último en nuestra aplicación nos tocaría que cada acción despachada se envíe por WebSockets en el canal action de forma que llegue al servidor y se actualice el Store ahí guardado.

Una última idea podría ser implementar un middleware en nuestro Store del lado del servidor que se encargue de guardar en una base de datos el payload de cada acción para que no se pierdan datos si se cae el servidor.

Conclusión

Este ejemplo es super simple, y no recomiendo que se use tal cual en producción. En una aplicación de verdad donde queramos replicar el Store en el servidor lo ideal sería que nuestro servidor de WebSockets nos mande el estado inicial del Store al conectarnos y luego cada acción que se realice, las cuales deberían crearse en el cliente que genera la acción y mandarlas por socket.io.

De esta forma en el cliente solo actualicemos lo necesario y no todo el estado de nuestra aplicación de golpe, esto reduciría la cantidad de datos enviados por WebSockets (menor consumo de datos en móviles), de la misma forma el servidor debería despachar a su Store propio la acción, así el estado ahí almacenado se mantendría actualizado para la próxima persona en conectarse y mientras cada cliente tiene su propio Store y se encarga de actualizarse.

Un problema que podría llegar a ocurrir de usar esta forma es que si un cliente se desconecta no le llegarían algunas acciones, lo cual puede significar una perdida de datos y en dejar de estar sincronizado con el Store. Esto se puede solucionar ya sea enviando todo el estado cada X tiempo o crear alguna especie de cola de acciones cuando el servidor detecte una desconexión del cliente.

Renderizando aplicaciones de Redux en el servidor

Renderizar en el servidor una aplicación hecha con React.js nos da una gran mejora de performance, o más bien de percepción de performance, lo cual de cara al usuario se convierte en una mejor UX al parecer que el sitio carga más rápido.

Incluso gracias a renderizar en el servidor es posible hacer aplicaciones que funcionen sin JS (Server First) y que una vez descargado e iniciado JS funcionen como una aplicación de una página (SPA).

Cuando es solo una aplicación en React.js es fácil realizarlo. Pero cuando lo combinamos con Redux necesitamos crear una instancia del Store del mismo en cada petición para que esto funcione.

Instalando dependencias

Primero, como siempre, vamos a instalar nuestras dependencias.

1 npm i -S react react-dom react-redux redux micro
  • micro librería para crear micro servicios HTTP.

Preparando el servidor

Una vez instaladas vamos a crear un servidor muy básico. El servidor lo vamos a crear como un micro servicio, de esa forma podemos renderizar nuestra aplicación sin importar si usamos Node.js o no como backend.

 1 // cargamos micro para crear el server
 2 import micro, { json, send, sendError } from "micro";
 3 async function serverRender(request, response) {
 4   try {
 5     // obtenemos el body del request para saber los datos
 6     const body = await json(request);
 7     // mandamos la respuesta
 8     return send(response, 200, "hola mundo");
 9   } catch (error) {
10     // si hay un error mandamos el error
11     sendError(request, response, error);
12   }
13 }
14 const server = micro(serverRender);
15 server.listen(process.env.PORT || 3000);

Ese servidor va a ser nuestra base. Dentro vamos a colocar el código. Pero primero vamos a explicar como va a funcionar esto.

Cuando el usuario entre a nuestra aplicación, digamos que en Django, vamos recibir la petición, Django se tiene que encargar de obtener todos los datos necesarios para nuestra vista y va a mandar una petición HTTP a nuestro micro servicio.

Nuestro micro servicio entonces va a renderizar el HTML que corresponde y va a devolvérselo a Django para que lo inyecte en alguna plantilla HTML y lo mande al usuario, luego ya podríamos renderizar en el navegador y que funcione como una aplicación de una página.

Renderizando React.js

Lo primero es que vamos a definir que datos va a recibir nuestro micro servicio.

 1 type body = {
 2   component: string, // el path al componente
 3   props: Object // los datos necesarios
 4 };
 5 // ejemplo
 6 const body = {
 7   component: "/build/server/Home.js",
 8   props: {
 9     list: []
10   }
11 };

Ahora que ya sabemos esto vamos a hacer que nuestro micro servicio renderice React.

 1 import micro, { json, send, sendError } from "micro";
 2 import React from "react";
 3 import { renderToString } from "react-dom/server";
 4 async function serverRender(request, response) {
 5   try {
 6     // obtenemos component y props del body
 7     const { component, props } = await json(request);
 8     // cargamos el componente que recibimos
 9     const Component = require(component);
10     // lo renderizamos a HTML
11     const html = renderToString(<Component {...props} />);
12     // respondemos con el HTML
13     return send(response, 200, html);
14   } catch (error) {
15     sendError(request, response, error);
16   }
17 }
18 const server = micro(serverRender);
19 server.listen(process.env.PORT || 3000);

Con esto ya tenemos un pequeño micro servicio que al recibir un request con el componente a renderizar y los datos necesarios devuelve el HTML generado.

Implementando Redux

Si queremos usar Redux en nuestra aplicación, para poder usarlo en el servidor vamos a necesitar instanciar un Store de Redux por cada request y darle ese Store a nuestra aplicación de React.

Para hacer esto podemos hacer que nuestro servidor se encargue de crear el Store y luego de mandárselo a React, pero ya que nuestro micro servicio no sabe que estamos renderizando, solo recibe el componente y los datos y renderiza, no nos sirve hacer esto.

Para nuestro caso lo que vamos a hacer es que el componente que renderizamos reciba los datos que genera la instancia del servidor, algo similar a esto:

 1 import React from "react";
 2 import { createStore } from "redux";
 3 import { Provider } from "react-redux";
 4 import reducer from "my-app/reducer.js";
 5 import App from "my-app/containers/App.jsx";
 6 function ServerProvider(props) {
 7   const store = createStore(reducer, props);
 8   return (
 9     <Provider store={store}>
10       <App />
11     </Provider>
12   );
13 }
14 export default ServerProvider;

De esta forma el entry point de nuestra aplicación para servidor va a ser nuestro componente ServerProvider, el cual va a recibir como props el estado inicial de la aplicación, va a crear el Store y devolver una aplicación de React conectada a Redux.

Con esto hecho de esta forma ya ni siquiera necesitamos modificar nuestro servidor, ya que con solo pasarle el path a nuestro ServerProvider y el estado inicial ya tenemos todo listo para generar el HTML para hacer server-render.

Renderizado con props

Puede pasar, y es muy común, que nuestro componente App reciba sus propios props, datos que no es necesario o no tiene sentido que estén guardados en el estado global (por ejemplo si son inmutables).

En ese caso nuestro micro servicio no nos permite usar esa funcionalidad, así que vamos a modificar tanto el ServerProvider como el micro servicio para poder realizarlo.

1 function ServerProvider(props) {
2   const store = createStore(reducer, props.initialState);
3   return (
4     <Provider store={store}>
5       <App {...props.initialProps} />
6     </Provider>
7   );
8 }

Ahora nuestro ServerProvider va a recibir dos datos vía props, el primero es el initialState el cual es usado para crear el Store de Redux. El segundo es initialProps el cual es usado como los props de nuestro componente App.

 1 async function serverRender(request, response) {
 2   try {
 3     // obtenemos component, initialState e initialProps del body
 4     const { component, initialState, initialProps = {} } = await json(request);
 5     // cargamos el componente que recibimos
 6     const Component = require(component);
 7     // lo renderizamos a HTML pasando
 8     // el estado y los props iniciales
 9     const html = renderToString(
10       <Component initialState={initialState} initialProps={initialProps} />
11     );
12     // respondemos con el HTML
13     return send(response, 200, html);
14   } catch (error) {
15     sendError(request, response, error);
16   }
17 }

Con esta modificación a nuestro micro servicio podemos obtener de los datos que recibimos de la petición el estado y los props iniciales y mandarlos al componente (nuestro ServerProvider) para que renderice la aplicación y gracias al valor por defecto del initialProps en caso de no recibir nada igual va a funcionar.

Conclusión

Como se puede ver renderizar una aplicación con React y Redux no es complicado y solo es necesario realizar un paso más que si usáramos solo React y los beneficios para la UX son muy buenos gracias a que el usuario desde el primer momento puede recibir datos.

Mi recomendación es que siempre rendericen en el servidor sus aplicaciones, incluso que apliquen la metodología Server-First o Progressive Enhancement para que su aplicación no requiere de JS para sus funciones básicas. Con React y Redux renderizar en el servidor es más fácil que nunca.

Obteniendo datos en aplicaciones de Redux

Ya sabemos como usar Redux y como despachar acciones para modificar el estado, pero ¿Qué pasa si queremos traernos más datos desde el servidor?

Es muy común que esto ocurra ya que nuestras aplicaciones web interactúan con un servidor constantemente mediante peticiones HTTP, ya sea usando AJAX o Fetch.

Definiendo el API

Para poder hacer esto vamos a suponer que tenemos un API de artículos de un blog. Nuestros endpoint van a ser algo así:

1 GET    /api/v1/posts/
2 POST   /api/v1/posts/
3 GET    /api/v1/posts/:id
4 PUT    /api/v1/posts/:id
5 DELETE /api/v1/posts/:id

Creando un cliente para el API

Lo primero que vamos a hacer es crear un objeto para consumir este API

 1 import "isomorphic-fetch";
 2 const endpoint = "/api/v1/posts";
 3 const api = {
 4   posts: {
 5     async read(id = null) {
 6       try {
 7         if (!id) {
 8           const response = await fetch(endpoint);
 9           const data = await response.json();
10           return Promise.resolve(data);
11         }
12         const response = await fetch(`${endpoint}/${id}`);
13         const data = await response.json();
14         return Promise.resolve(data);
15       } catch (error) {
16         return Promise.reject(error);
17       }
18     },
19     async create(data) {
20       try {
21         const response = await fetch({
22           url: endpoint,
23           method: "POST",
24           body: data
25         });
26         const data = await response.json();
27         return Promise.resolve(data);
28       } catch (error) {
29         return Promise.reject(error);
30       }
31     },
32     async update(id, data) {
33       try {
34         const response = await fetch({
35           url: `${endpoint}/${id}`,
36           method: "PUT",
37           body: data
38         });
39         const data = await response.json();
40         return Promise.resolve(data);
41       } catch (error) {
42         return Promise.reject(error);
43       }
44     },
45     async delete(id) {
46       try {
47         const response = await fetch({
48           url: `${endpoint}/${id}`,
49           method: "DELETE"
50         });
51         const data = await response.json();
52         return Promise.resolve(data);
53       } catch (error) {
54         return Promise.reject(error);
55       }
56     }
57   }
58 };
59 export default api;

Este objeto nos va a servir como cliente para consumir el api, de esta forma podemos hacer peticiones con líneas como api.posts.read(1) el cual devolvería el post con el ID uno.

Middleware para acciones asíncronas

Ya que trabajamos con Redux tiene sentido que nuestras peticiones sean acciones, en este caso asíncronas. Para poder trabajar de esta forma vamos a crear un pequeño middleware que nos permita despachar acciones asíncronas.

 1 function asyncAwait(asyncResolver) {
 2   return store => next => action => {
 3     // despachamos la acción normalmente
 4     const result = next(action);
 5     // ejecutamos el asyncResolver pasándole
 6     // la acción y la función dispatch
 7     asyncResolve(action, store.dispatch);
 8     // retornamos la acción despachada
 9     return result;
10   };
11 }

Este pequeño middleware nos va a permitir despachar acciones y que estas lleguen a una función que llamamos asyncResolver, la misma va a ser una función asíncrona que al recibir ciertas acciones va a empezar el proceso asíncrono para obtener datos.

 1 import api from 'my-app/utils/api.js';
 2 async function requestPost(payload, dispatch) {
 3   try {
 4     // hacemos el request
 5     const data = await api.posts.read(payload.id);
 6     // despachamos una acción con los datos
 7     dispatch({
 8       type: 'POST_SUCCEED'
 9       payload: data,
10     });
11   } catch (error) {
12     // despachamos el error para mostrarlo en la UI
13     return dispatch({
14       type: 'POST_FAILED',
15       payload: error.message,
16       error: true,
17     });
18   }
19 }
20 async function asyncResolve(action, dispatch) {
21   switch (action.type) {
22     case 'POST_REQUEST':
23       return requestPost(action.payload, dispatch);
24     default:
25       return action;
26   }
27 }
28 export default asyncResolver;

Como vemos en el código de arriba, el asyncResolver al igual que los reducer se encarga de verificar cual es el tipo de acción que recibimos y se encarga realizar la petición y despachar una acción con al respuesta, o en caso de un error despachar el mensaje para que el usuario se pueda enterar.

Implementando el Middleware

Por último, necesitamos hacer que nuestro store sepa que existe el middleware y le pase las acciones.

 1 import { createStore, applyMiddleware } from "redux";
 2 import reducer from "my-app/reducer.js";
 3 import asyncAwait from "my-app/async-await.js";
 4 import asyncResolver from "my-app/async-resolver";
 5 export default initialState =>
 6   createStore(
 7     reducer,
 8     initialState,
 9     applyMiddleware(asyncAwait(asyncResolver))
10   );

Con esto podemos crear un archivo store.js que reciba el estado inicial y nos devuelva un store aplicándole el middleware que creamos y nuestro asyncProvider.

Conclusión

Ahora ya estamos listos para empezar a despachar acciones para iniciar peticiones y que luego el asyncProvider sepa que tiene que hacer para cada tipo de acción y actuar en consecuencia.

Por último, la forma en que estamos centralizando todo es más o menos como funciona Redux Saga, una de las librerías más populares para trabajar con flujos de datos asíncronos haciendo uso de los Generadores de ES2015.

Estado inmutable con Redux e Immutable.js

Redux nos propone tratar nuestro estado como inmutable. Sin embargo los objetos y array en JavaScript no lo son, lo que puede causar que mutemos directamente el estado por error.

Immutable.js es una librería creada por Facebook para usar colecciones de datos inmutables como listas, mapas, sets, etc. Usándolo con Redux nos permite expresar nuestro estado como colecciones de Immutable.js para evitarnos estos posibles problemas al mutar datos.

Usándolo en un reducer

La primera forma de usar Immutable.js en Redux es usándolo directo en un reducer. Simplemente definiendo el estado inicial como una colección inmutable y luego modificándolo según la acción despachada.

 1 import { Map as map } from "immutable";
 2 function reducer(state = map(), { type, payload }) {
 3   switch (type) {
 4     case "ADD": {
 5       return state.set(payload.id.toString(), map(payload));
 6     }
 7     default: {
 8       return state;
 9     }
10   }
11 }
12 export default reducer;

De esta forma podemos empezar a hacer uso de Immutable.js. Un pequeño detalle al usar mapas inmutables es que el key usado debe ser siempre un string, puede ser un número, pero por experiencia esto pueda dar errores de que Immutable.js no encuentre el valor al hacer collection.get(1), por esa razón cuando agregamos el dato a nuestro mapa usamos .toString()sobre el ID para evitarnos este problema.

Combinando reducers

Aunque es posible tener un único reducer para toda la aplicación, a medida que esta crece lo común es empezar a dividirlo en múltiples reducers y usar redux.combineReducers para unirlos en uno solo que usamos al crear el Store.

1 import { combineReducers } from "redux";
2 import data from "./reducers/data.js";
3 export default combineReducers({
4   data
5 });

De esta forma nuestro estado ahora es un objeto con una propiedad data la cual posee nuestra colección inmutable, pero ¿Qué pasa si queremos que todo nuestro estado sea un conjunto de colecciones inmutables anidadas?

Combinando reducers con Immutable.js

Si decidimos tratar todo el estado como una colección inmutable debemos entonces hacer uso de redux-immutable. Esta librería nos ofrece una función combineReducers personalizada la cual funciona con exactamente la misma API que la oficial de Redux, por lo que hacer el cambio de una a otra consiste en cambiar de donde importamos la función.

1 import { combineReducers } from "redux-immutable";
2 import data from "./reducers/data.js";
3 export default combineReducers({
4   data
5 });

Como vemos simplemente pasamos de importar desde redux a hacerlo desde redux-immutable, con ese simple cambio estamos usando Immutable.js en todo nuestro store, ahora cuando conectemos nuestros componentes a este podemos usar una sintaxis 100% de Immutable.js.

1 function getItem(state, props) {
2   return state
3     .get('data')
4     .get(props.id.toString())
5     .toJS(),
6 }

Ese selector por ejemplo se encarga de traerse del mapa de datos el item con el ID recibido como prop y devolverlo convertido a un objeto de JS común que podemos recibir en un componente y usarlo sin problemas.

Conclusión

Usar Immutable.js nos permite trabajar con un estado verdaderamente inmutable evitando problemas comunes como pueden ser mutar directamente una propiedad sin crear una copia del estado lo cual puede causar errores de inconsistencia de datos y dolores de cabeza a muchos desarrolladores.

Además que Immutable.js es bastante fácil de usar por lo que incluso nos facilita nuestro trabajo como desarrolladores enormemente, por lo que vale mucho la pena empezar a usarlo.

Componentes de Alto Orden en React.js

Algo que ocurre muy seguido es que varios componentes de React vayan a necesitar usar una misma funcionalidad o extenderse con funciones de terceros (como el acceso a un Store o internacionalización).

Originalmente esto se lograba gracias a la utilización de Mixins, estos eran, básicamente, objetos que poseían los métodos que queríamos compartir, un ejemplo (de la misma documentación).

 1 var SetIntervalMixin = {
 2   componentWillMount() {
 3     this.intervals = [];
 4   },
 5   setInterval() {
 6     this.intervals.push(setInterval.apply(null, arguments));
 7   },
 8   componentWillUnmount() {
 9     this.intervals.forEach(clearInterval);
10   }
11 };

Este mixin contiene la lógica para poder crear fácilmente un intervalo (como con window.setInterval) el cual se borre automáticamente al desmontarse el componente, evitándonos tener que pensar en hacer esto a mano y en causar problemas de memoria si nos olvidamos.

Resulta que desde que se incorporaron las clases como forma de crear componentes, y más aún con las funciones para componentes puros, ya no es posible usar mixins en React.js.

Mixins Are Dead. Long Live Composition por Dan Abramov

Las razones para esto fueron que ES2015 no tiene soporte a mixins de forma nativa por lo que en vez de crear una API propia decidieron no soportarlos. El hacer esto significó tener que buscar nuevas formas de extender componentes para agregar funcionalidades comunes.

La solución llego desde el lado de la programación funcional gracias a las Funciones de Alto Orden. Estas son funciones que reciben una o más funciones como argumentos y devuelven una nueva función.

En React esto se traslada a Componentes de Alto Orden. Haciendo un paralelismo, es una función que recibe uno o más componentes y devuelve uno nuevo. Veamos un ejemplo super básico.

 1 import React from "react";
 2 function getViewport() {
 3   return {
 4     height: window.innerHeight,
 5     width: window.innerWidth
 6   };
 7 }
 8 // nuestro componente de alto orden
 9 function withViewport(WrappedComponent) {
10   return function WithViewport(props) {
11     return <WrappedComponent getViewport={getViewport} {...props} />;
12   };
13 }
14 export default withViewport;

Como vemos en el ejemplo tenemos una función getViewport el cual nos devuelve un objeto con el width y height de nuestro navegador y nuestro HOC (High Order Component — Componente de Alto Orden) que recibe un componente y devuelve un nuevo componente puro, el cual a su vez le pasa los props que recibe al componente envuelto y adicionalmente la función getViewport.

Ahora nuestro WrappedComponent recibiría, además de sus props normales, una función llamada getViewport. De esta forma muy simple podemos empezar a extender la funcionalidad de nuestros componentes igual que hacíamos con los mixins. Volviendo al ejemplo anterior de mixins veamos ahora como haríamos eso mismo usando un HOC.

 1 import React, { Component } from 'react';
 2 function setIntervalHOC(WrappedComponent) {
 3   return class WithSetInterval extends Component {
 4     componentWillMount() {
 5       this.intervals = [];
 6     }
 7     setInterval(..args) {
 8       const id = setInterval.apply(null, args);
 9       this.intervals.push(id);
10       return id;
11     }
12     componentWillUnmount() {
13       this.intervals.forEach(clearInterval);
14     }
15     render() {
16       return (
17         <WrappedComponent
18           setInterval={this.setInterval.bind(this)}
19           {...this.props}
20         />
21       );
22     }
23   }
24 }

Esa es la versión HOC del mixin para usar setInterval, la diferencia es que ahora es una función que recibe un componente y lo renderiza pasándole el setInterval propio, y es el componente WithSetInterval el cual posee la lista de intervalos y se encarga de borrarlos al desmontarse.

De esta forma el componente envuelto solo sabe que si llama props.setInterval va a crear un intervalo y que automáticamente se va a borrar al desmontarse el componente. Veamos por último como lo usaríamos:

 1 // ./components/App.jsx
 2 import React, { Component } from "react";
 3 import setIntervalHOC from "./decorators/set-interval.js";
 4 class App extends Component {
 5   static propTypes = {
 6     timer: PropTypes.number
 7   };
 8   static defaultProps = {
 9     timer: 500
10   };
11   state = {
12     amount: 0
13   };
14   componentDidMount() {
15     this.props.setInterval(this.tick.bind(this), this.props.timer);
16   }
17   tick() {
18     this.setState({
19       amount: this.state.amount + 1
20     });
21   }
22   render() {
23     return <div>{this.state.amount}</div>;
24   }
25 }
26 export default setIntervalHOC(App);
1 // ./index.js
2 import React from "react";
3 import { render } from "react-dom";
4 import App from "./components/App.jsx";
5 render(<App timer={1000} />, document.getElementById("app"));

Como vemos simplemente creamos nuestro componente que haga uso del intervalo y se exporte envuelto en setIntervalHOC, de esa forma cuando importemos App vamos a importar en realidad el componente devuelto por el HOC y al renderizarse mostraría primero un 0, luego de un segundo un 1, y así, cada segundo (o lo que hayamos indicado a la propiedad timer de App, o si no indicamos nada 500ms),iría aumentando hasta que se desmonte.

Gist con el ejemplo:
https://gist.github.com/sergiodxa/09fa274d68c929a4059bdb8000c03e49

Conclusión

Los Componentes de Alto Orden son una forma excelente, y fácil de usar, para extender componentes. Este patrón es usado por ejemplo en React Redux para conectar un componente al Store de Redux.

Hay otras formas de usarlos además de la vista acá, el patrón que usamos se conoce como PropsProxy ya que estamos manipulando los props que llega al componente. Otro patrón es Inheritance Inversion que consiste en devolver un nuevo componente que extienda el componente que estamos envolviendo.

Como detalle extra, los HOC se pueden usar como decoradores siguiendo la propuesta actual (y capaz obsoleta), permitiendo usarlos de esta forma:

1 @setIntervalHOC
2 class App extends Component { ... }
3 export default App;

Migrando a Redux

Redux no es un framework monolítico, sino un conjunto de contratos y algunas funciones que hacen que todo funcione en conjunto. La mayor parte de tu “código de Redux” ni siquiera va a hacer uso de la API de Redux, ya que la mayor parte del tiempo vas a crear funciones.

Esto hace fácil migrar a o desde Redux.

¡No queremos encerrarte!

Desde Flux

Los reducers capturan “la esencia” de los Stores de Flux, así que es posible migrar gradualmente de un proyecto Flux existente a uno de Redux, ya sea que uses Flummox, Alt, Flux tradicional o cualquier otra librería de Flux.

También es posible hacer lo contrario y migrar de Redux a cualquier de estas siguiendo los siguiente pasos:

Tu proceso debería ser algo como esto:

Crea una función llamada createFluxStore(reducer) que cree un store de Flux compatible con tu aplicación actual a partir de un reducer. Internamente debería ser similar a la implementación de createStore (código fuente) de Redux. Su función dispatch solo debería llamar al reducer por cada acción, guardar el siguiente estado y emitir el cambio.

Esto te permite gradualmente reescribir cada Store de Flux de tu aplicación como un reducer, pero todavía exportar createFluxStore(reducer) así el resto de tu aplicación no se entera de que esto esta ocurriendo con los Stores de Flux.

Mientras reescribes tus Stores, vas a darte cuenta que deberías evitar algunos anti-patrones de Flux como peticiones a APIs dentro del Store, o ejecutar acciones desde el Store. Tu código de Flux va a ser más fácil de entender cuando lo modifiques para que funcione en base a reducers.

Cuando hayas portado todos los Stores de Flux para que funcionen con reducers, puedes reemplazar tu librería de Flux con un único Store de Redux, y combinar estos reducers que ya tienes usando combineReducers(reducers)
.
Ahora lo único que falta es portar la UI para que use react-redux o alguno similar.
Finalmente, seguramente quieras usar cosas como middlewares para simplificar tu código asíncrono.

Desde Backbone

La capa de modelos de Backbone es bastante diferente de Redux, así que no recomendamos combinarlos. Si es posible, lo mejor es reescribir la capa de modelos de tu aplicación desde cero en vez de conectar Backbone a Redux. Igualmente, si no es posible reescribirlo, capaz debería usar backbone-redux para migrar gradualmente, y mantener tu Store de Redux sincronizado con los modelos y colecciones de Backbone.

Glosario de términos

Este es un glosario de los términos principales en Redux, junto a su tipo de dato. Los tipos están documentados usando la notación Flow.

Estado

1 type State = any;

Estado (también llamado árbol de estado) es un termino general, pero en la API de Redux normalmente se refiere al valor de estado único que es manejado por el Store y devuelto por getState(). Representa el estado de tu aplicación de Redux, normalmente es un objeto con muchas anidaciones.

Por convención, el estado a nivel superior es un objeto o algún tipo de colección llave-valor como un Map, pero técnicamente puede ser de cualquier tipo. Aun así, debes hacer tu mejor esfuerzo en mantener el estado serializable. No pongas nada dentro que no puedas fácilmente convertirlo a un JSON.

Acción

1 type Action = object;

Una acción es un objeto plano (POJO — Plan Old JavaScript Object) que representa una intención de modificar el estado. Las acciones son la única forma en que los datos llegan al store. Cualquier dato, ya sean eventos de UI, callbacks de red, u otros recursos como WebSockets eventualmente van a ser despachados como acciones.

Las acciones deben tener un campo type que indica el tipo de acción a realizar. Los tipos pueden ser definidos como constantes e importados desde otro módulo Es mejor usar strings como tipos en vez de Symbols ya que los strings son serializables.

Aparte del type, la estructura de una acción depende de vos. Si estás interesado, revisa Flux Standard Action para recomendaciones de como deberías estar estructurado una acción.

Revisa acción asíncrona debajo.

Reducer

1 type Reducer<S, A> = (state: S, action: A) => S;

Un reducer (también llamado función reductora) es una función que acepta una acumulación y un valor y devuelve una nueva acumulación. Son usados para reducir una colección de valores a un único valor.

Los reducers no son únicos de Redux — son un concepto principal de la programación funcional. Incluso muchos lenguajes no funcionales, como JavaScript, tienen una API para reducción. En JavaScript, es Array.prototype.reduce().

En Redux, el valor acumulado es el árbol de estado, y los valores que están siendo acumulados son acciones. Los reducers calculan el nuevo estado en base al anterior estado y la acción. Deben ser funciones puras — funciones que devuelven el mismo valor dados los mismos argumentos. Deben estar libres de efectos secundarios. Esto es lo que permite características increíbles como hot reloading y time travel.

Los reducers son el concepto más importante en Redux.

No hagas peticiones a APIs en los reducers.

Función despachadora

1 type BaseDispatch = (a: Action) => Action;
2 type Dispatch = (a: Action | AsyncAction) => any;

La función despachadora (o simplemente función dispatch) es una función que acepta una acción o una acción asíncrona; entonces puede o no despachar una o más acciones al store.

Debemos distinguir entre una función despachadora en general y la función base dispatch provista por la instancia del store sin ningún middleware.

La función base dispatch siempre envía síncronamente acciones al reducer del store, junto al estado anterior devuelto por el store, para calcular el nuevo estado. Espera que las acciones sean objetos planos listos para ser consumidos por el reducer.

Los middlewares envuelven la función dispatch base. Le permiten a la función dispatch manejar acciones asíncronas además de las acciones. Un middleware puede transformar, retrasar, ignorar o interpretar de cualquier forma una acción o acción asíncrona antes de pasarla al siguiente middleware. Lea más abajo para más información.

Creador de acciones

1 type ActionCreator = (...args: any) => Action | AsyncAction;

Un creador de acciones es, simplemente, una función que devuelve una acción. No confunda los dos términos — otra vez, una acción es un pedazo de información, y los creadores de acciones son fabricas que crean esas acciones.

Llamar un creador de acciones solo produce una acción, no la despacha. Necesitas llama al método dispatch del store para causar una modificación. Algunas veces decimos creador de acciones conectado, esto es una función que ejecuta un creador de acciones e inmediatamente despacha el resultado a una instancia del store específica.

Si un creador de acciones necesita leer el estado actual, hacer una llamada al API, o causar un efecto secundario, como una transición de rutas, debe retornas una acción asíncrona en vez de una acción.

Acción asíncrona

1 type AsyncAction = any;

Una acción asíncrona es un valor que es enviado a una función despachadora, pero todavía no esta listo para ser consumido por el reducer. Debe ser transformada por un middleware en una acción (o una serie de acciones) antes de ser enviada a la función dispatch() base. Las acciones asíncronas pueden ser de diferentes tipos, dependiendo del middleware que uses. Normalmente son primitivos asíncronos como una promesa o un thunk, que no son enviados inmediatamente a un reducer, pero despachan una acción cuando una operación se completa.

Middleware

1 type MiddlewareAPI = { dispatch: Dispatch, getState: () => State };
2 type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch;

Un middleware es una función de orden superior que toma una función despachadora y devuelve una nueva función despachadora. A menudo convierten acciones asíncronas en acciones.

Los middlewares son combinables usando funciones. Son útiles para registrar acciones, realizar efectos secundarios como ruteo, o convertir una llamada asíncrona a una API en una serie de acciones síncronas.

Revisa applyMiddleware(...middlewares) para más detalles de los middlewares.

Store

1 type Store = {
2   dispatch: Dispatch
3   getState: () => State
4   subscribe: (listener: () => void) => () => void
5   replaceReducer: (reducer: Reducer) => void
6 }

Un store es un objeto que mantiene el árbol de estado de la aplicación.

Solo debe haber un único store en una aplicación de Redux, ya que la composición ocurre en los reducers.

  • dispatch(action) es la función dispatch base descrita arriba.
  • getState() devuelve el estado actual de la aplicación.
  • subscribe(listener) registra una función para que se ejecute en cada cambio de estado.
  • replaceReducer(nextReducer) puede ser usada para implementar hot reloading y code splitting. Normalmente no la vas a usar.

Revisa la referencia del API del Store completa para más detalles.

Creador de store

1 type StoreCreator = (reducer: Reducer, initialState: ?State) => Store;

Un creador de store es una función que crea un store de Redux. Al igual que la función despachante, debemos separar un creador de stores base, createStore(reducer, initialState) exportado por Redux, por los creadores de store devueltos por los potenciadores de store.

Potenciador de store

1 type StoreEnhancer = (next: StoreCreator) => StoreCreator;

Un potenciador de store es una función de orden superior que toma un creador de store y devuelve una versión potenciada del creador de store. Es similar a un middleware ya que te permite alterar la interfaz de un store de manera combinable.

Los potenciadores de store son casi el mismo concepto que los componentes de orden superior de React, que ocasionalmente se los llamada “potenciadores de componentes”.

Debido a que el store no es una instancia, sino una colección de funciones en un objeto plano, es posible crear copias fácilmente y modificarlas sin modificar el store original. Hay un ejemplo en la documentación de compose demostrándolo.

Normalmente nunca vas a escribir un potenciador de store, pero capaz uses el que provee las herramientas de desarrollo. Es lo que permite que el time travel sea posible sin que la aplicación se entere de que esta ocurriendo. Curiosamente, la forma de implementar middleware en Redux es un potenciador de store.