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í:

GET    /api/v1/posts/
POST /api/v1/posts/
GET /api/v1/posts/:id
PUT /api/v1/posts/:id
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

import 'isomorphic-fetch';
const endpoint = '/api/v1/posts';
const api = {
posts: {
async read(id = null) {
try {
if (!id) {
const response = await fetch(endpoint);
const data = await response.json();
return Promise.resolve(data);
}
const response = await fetch(`${endpoint}/${id}`);
const data = await response.json();
return Promise.resolve(data);
} catch (error) {
return Promise.reject(error);
}
},
async create(data) {
try {
const response = await fetch({
url: endpoint,
method: 'POST',
body: data,
});
const data = await response.json();
return Promise.resolve(data);
} catch (error) {
return Promise.reject(error);
}
},
async update(id, data) {
try {
const response = await fetch({
url: `${endpoint}/${id}`,
method: 'PUT',
body: data,
});
const data = await response.json();
return Promise.resolve(data);
} catch (error) {
return Promise.reject(error);
}
},
async delete(id) {
try {
const response = await fetch({
url: `${endpoint}/${id}`,
method: 'DELETE',
});
const data = await response.json();
return Promise.resolve(data);
} catch (error) {
return Promise.reject(error);
}
},
},
};
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.

function asyncAwait(asyncResolver) {
return store => next => action => {
// despachamos la acción normalmente
const result = next(action);
// ejecutamos el asyncResolver pasándole
// la acción y la función dispatch
asyncResolve(action, store.dispatch);
// retornamos la acción despachada
return result;
};
}

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.

import api from 'my-app/utils/api.js';
async function requestPost(payload, dispatch) {
try {
// hacemos el request
const data = await api.posts.read(payload.id);
// despachamos una acción con los datos
dispatch({
type: 'POST_SUCCEED'
payload: data,
});
} catch (error) {
// despachamos el error para mostrarlo en la UI
return dispatch({
type: 'POST_FAILED',
payload: error.message,
error: true,
});
}
}
async function asyncResolve(action, dispatch) {
switch (action.type) {
case 'POST_REQUEST':
return requestPost(action.payload, dispatch);
default:
return action;
}
}
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.

import { createStore, applyMiddleware } from 'redux';
import reducer from 'my-app/reducer.js';
import asyncAwait from 'my-app/async-await.js';
import asyncResolver from 'my-app/async-resolver';
export default initialState => createStore(
reducer,
initialState,
applyMiddleware(
asyncAwait(asyncResolver)
)
);

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.