Asynchronous Data Flow

Redux-thunk: basics

While redux itself is entirely synchronous, you can use a middleware such as redux-thunk to handle asynchronous actions.

A “thunk” is another name for a callback. It is a function that is usually passed as an argument to be called at a later time.

To use, apply the middleware to your redux store:

import ReduxThunk from 'redux-thunk';

const store = createStore(
    reducer,
    applyMiddleware(ReduxThunk)
);

This allows you to pass a thunk to dispatch instead of a plain object. The middleware will recognize the thunk and call it. The thunk takes the store’s dispatch method as a parameter:

// an asynchronous action - "thunk"
// This will wait 1 second, and then dispatch the 'INCREMENT' action
const delayedIncrement = dispatch => setTimeout(() => {
    dispatch({
        type: 'INCREMENT'
    });
}, 1000);

// dispatch the thunk. 
// note: no () as we're passing the function itself
store.dispatch(delayedIncrement);

Using redux-thunk with jQuery.ajax

const loadUser = userId => dispatch => {
    dispatch({ type: 'USER_LOADING' });
    $.ajax('/users/' + userId, {
        type: 'GET',
        dataType : 'json'
    }).done(response => {
        dispatch({ type: 'USER_LOADED', user: response });
    }).fail((xhr, status, error) => {
        dispatch({ type: 'USER_LOAD_ERROR', status, error });
    });
};

To use, dispatch like any other action creator:

store.dispatch(loadUser(123));

This will result in an initial USER_LOADING action being dispatched, which can be used to display a loading indicator (if so desired), and after the response has been received either a USER_LOADED action or USER_LOAD_ERROR action will be dispatched, depending on the result of the $.ajax request.

Middleware

When you call store.dispatch(actionObject) it is handled synchronously. I.e. reducers would be called and your store listeners would be notified, your react views would be re-rendered on each dispatched action.

Middleware is what enables you to delay dispatching or even dispatch different actions in the middle. I.e. middleware makes your asynchronous actions look synchronous.

const myAsyncMiddleware = (store) => {
    return (next) => {
        return (action) => {
            if(action.type === "ASYNC_ACTION") {
              setTimeout(() => {
                store.dispatch({ type: "ASYNC_ACTION_RESPONSE" });
              }, 1000);
            } else {
              return next(action);
            }
        }
    }
}

const store = createStore(
  reducer,
  applyMiddleware(myAsyncMiddleware)
);

Action creators

Another approach to handling asynchrony in Redux is to use action creators. In Flux, action creators are special functions that construct action objects and dispatch them.

myActionCreator(dispatch) {
  dispatch({ type: "ASYNC_ACTION_START" });
  setTimeout(() => {
    dispatch({ type: "ASYNC_ACTION_END" });
  }, 1000)
}

Use custom middleware + Superagent

This is example has extracted from this boilerplate.

Custom middleware:

export default function clientMiddleware() {
  return ({dispatch, getState}) => {
    return next => action => {
      if (typeof action === 'function') {
        return action(dispatch, getState);
      }

      const { promise, types, ...rest } = action; // eslint-disable-line no-redeclare
      if (!promise) {
        return next(action);
      }

      const [REQUEST, SUCCESS, FAILURE] = types;
      next({...rest, type: REQUEST});

      const client = new ApiClient();
      const actionPromise = promise(client);
      actionPromise.then(
        (result) => next({...rest, result, type: SUCCESS}),
        (error) => next({...rest, error, type: FAILURE})
      ).catch((error)=> {
        console.error('MIDDLEWARE ERROR:', error);
        next({...rest, error, type: FAILURE});
      });

      return actionPromise;
    };
  };
}

Wrapping Superagent library for API call:

import superagent from 'superagent';
import config from '../config';

const methods = ['get', 'post', 'put', 'patch', 'del'];

function formatUrl(path) {
  const adjustedPath = path[0] !== '/' ? '/' + path : path;
  return adjustedPath;
}

export default class ApiClient {
  constructor(req) {
    methods.forEach((method) =>
      this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => {
        const request = superagent[method](formatUrl(path));

        if (params) {
          request.query(params);
        }

        if (data) {
          request.send(data);
        }

        request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body));
      }));
  }
  empty() {}
}

Using redux-thunk with Promises

import 'whatwg-fetch';

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}

function parseJSON(response) {
  return response.json();
}

function getJSON(endpoint, params) {
  return fetch(endpoint, params)
    .then(checkStatus)
    .then(parseJSON);
}

export function action() {
  return dispatch => getJSON('/example-endpoint')
    .then((result) => {
      dispatch({
        type: GET_SUCCESS,
        result,
      });
    })
    .catch((error) => {
      dispatch({ type: GET_FAILURE, error });
    });
}