Acciones asíncronas en Redux.js

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

(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.

export default function addTodo(content) {
return {
type: 'ADD_TODO',
payload: {
content,
},
};
}

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

import store from './store';
import addTodo from './actions/add-todo';

fetch('/api/todos/1')
.then(response => response.json()) // obtenemos los datos
.then(addTodo) // creamos la acción
.then(store.dispatch) // despachamos la acción
.catch(error => {
// si hay algún error lo mostramos en consola
console.error(error)
});

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.

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.

import {
createStore,
applyMiddleware
} from 'redux';
import promise from 'redux-promise';
import thunk from 'redux-thunk';

import reducers from './reducers';

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.

import store from './store';
import addTodo from './actions/add-todo';
// creamos un pequeño cliente para un api
const api = {
todos: {
retrieve() {
// hacemos el request y nos encargamos de convertir
// la respuesta en una acción válida
return fetch('/api/todo')
.then(response => response.json()) // obtenemos los datos
.then(addTodo); // los convertimos en una acción
}
},
};

store.dispatch(api.todos.post());

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. En futuros artículos explicare más sobre como usarlo.

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.