There's a lot of misinformation on how to use async/await with React and Redux. In general, React does not support async/await, but you can make it work with some caveats. In particular, render() cannot be async, but React suspense may change this. For example, the below minimal example with Node.js 8.9.4, React 16.4.1, and React-DOM 16.4.1 will throw an error.

const { Component, createElement } = require('react');
const { renderToString } = require('react-dom/server');

class MyComponent extends Component {
  async render() {
    return null;
  }
}

// Throws "Invariant Violation: Objects are not valid as a React child
// (found: [object Promise])"
assert.throws(function() {
  renderToString(createElement(MyComponent));
}, /Objects are not valid as a React child/);

You can make lifecycle methods async, like componentWillMount(), in some cases. However, async componentWillMount() doesn't work at all with server-side rendering.

class MyComponent extends Component {
  async componentWillMount() {
    this.setState({ text: 'Before' });
    await new Promise(resolve => setImmediate(resolve));
    this.setState({ text: 'After' });
  }

  render() {
    return createElement('h1', null, this.state.text);
  }
}

// Prints '<h1 data-reactroot="">Before</h1>'
// Note React does **not** wait for the `componentWillMount()` promise
// to settle.
// Also prints "Warning: setState(...): Can only update a
// mounting component."
console.log(renderToString(createElement(MyComponent)));

Since React doesn't support async/await in general, you still need to use a library like redux and a middleware tool like redux-thunk to handle async/await. There are alternatives like redux-saga which I'll cover in another article.

Introducing Redux-Thunk

The redux-thunk framework is a Redux middleware that lets you dispatch a function which may or may not be async. The function takes a single parameter, a function dispatch(), which lets you dispatch actions. This function is called an action creator. Below is an example of dispatching actions from an async action creator using redux-thunk.

// Create a Redux store with `redux-thunk` middleware
const { applyMiddleware, createStore } = require('redux');
const thunk = require('redux-thunk').default;
const reducer = (state, action) => {
  switch(action.type) {
    case 'INCREMENT': return { count: state.count + 1 };
    case 'DECREMENT': return { count: state.count - 1 };
    default:          return state;
  };
};
const store = createStore(reducer, { count: 0 }, applyMiddleware(thunk));

// Create a React component that is tied to the store
class MyComponent extends Component {
  componentWillMount() {
    this.setState(store.getState());
    store.subscribe(() => this.setState(store.getState()));
  }

  render() {
    return createElement('h1', null, `Count: ${this.state.count}`);
  }
}

// Dispatch an async function. The `redux-thunk` middleware handles
// running this function.
store.dispatch(async function(dispatch) {
  dispatch({ type: 'INCREMENT' });
  // <h1 data-reactroot="">Count: 1</h1>
  console.log(renderToString(createElement(MyComponent)));

  await new Promise(resolve => setImmediate(resolve));

  dispatch({ type: 'DECREMENT' });
  // <h1 data-reactroot="">Count: 0</h1>
  console.log(renderToString(createElement(MyComponent)));
});

Side note: redux-thunk has nothing to do with the normal definition of a thunk as "a function that takes a single parameter, a callback". When working with React, a thunk is typically any function that encapsulates logic that will be run later, which is not a very useful definition. Broadly, React's definition of a thunk is a superset of the conventional JavaScript definition of a thunk.

What happens if your async thunk throws an error? Turns out redux-thunk has no built-in error handling, so you'll get an unhandled promise rejection.

const { applyMiddleware, createStore } = require('redux');
const thunk = require('redux-thunk').default;
// ...

store.dispatch(async function(dispatch) {
  dispatch({ type: 'INCREMENT' });
  // <h1 data-reactroot="">Count: 1</h1>
  console.log(renderToString(createElement(MyComponent)));

  await new Promise(resolve => setImmediate(resolve));

  // Unhandled promise rejection! See
  // http://bit.ly/unhandled-promise-rejection
  throw new Error('Oops!');
});

In other words, redux-thunk doesn't support async/await any more than React itself does. However, since redux-thunk accepts a function rather than relying on classes, you can create a wrap() function that ensures you dispatch() an error event if your async thunk errors out. This is the same pattern you need to use for handling async functions with Express.

const { applyMiddleware, createStore } = require('redux');
const thunk = require('redux-thunk').default;
const reducer = (state, action) => {
  switch(action.type) {
    case 'INCREMENT': return { count: state.count + 1 };
    case 'DECREMENT': return { count: state.count - 1 };
    case 'ERROR':     return { count: action.error.message };
    default:          return state;
  };
};
// ...

store.dispatch(wrap(async function(dispatch) {
  dispatch({ type: 'INCREMENT' });
  // <h1 data-reactroot="">Count: 1</h1>
  console.log(renderToString(createElement(MyComponent)));

  await new Promise(resolve => setImmediate(resolve));

  // Caught by the `wrap()` function and becomes an action with
  // `type: 'ERROR'`
  throw new Error('Oops!');
}));

// Wrap the async function and dispatch an error if an error occurred
function wrap(fn) {
  return function(dispatch) {
    fn(dispatch).catch(error => dispatch({ type: 'ERROR', error }));
  };
}

Next, let's take a look at an example of using a wrapper function with a more realistic React setup.

Running in the Browser

The previous examples are all minimal code samples designed to run in vanilla Node.js. They're meant to illustrate key points without unnecessary bloat. But, of course, the vast majority of React apps run in the browser, use JSX, and hook up Redux using react-redux.

First, install all the minimal tooling you need to compile and run a React app, as well as React and Redux themselves.

$ npm install babel-loader babel-preset-react webpack webpack-cli serve --save
$ npm install react react-dom redux-thunk redux --save

Minimal webpack.config.js:

module.exports = {
  entry: ['./index.js'],
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/i,
      use: {
          loader: 'babel-loader'
        }
    }]
  }
};

Minimal .babelrc:

{
  "presets": ["react"]
}

Minimal index.html:

<html>
  <body>
    <div id="container">
    </div>
    <script type="text/javascript" src="dist/main.js"></script>
  </body>
</html>

Below is the index.js file. This example uses wrap() to handle errors in async action creators. The below example uses a long-lived action creator that dispatches an 'INCREMENT' action after 5 seconds and then throws an error after another 5 seconds. The wrap() function handles the error and dispatches an error action.

import React from 'react';
import { applyMiddleware, createStore } from 'redux';
import dom from 'react-dom';
import thunk from 'redux-thunk';

// Set up Redux
const reducer = (state, action) => {
  switch(action.type) {
    case 'INCREMENT': return { count: state.count + 1 };
    case 'DECREMENT': return { count: state.count - 1 };
    case 'ERROR':     return { count: action.error.message };
    default:          return state;
  };
};
const store = createStore(reducer, { count: 0 }, applyMiddleware(thunk));

// Create React component
class MyComponent extends React.Component {
  componentWillMount() {
    this.setState(store.getState());
    store.subscribe(() => this.setState(store.getState()));
  }

  render() {
    return <h1>{this.state.count}</h1>;
  }
}

// Render the React component to the DOM
dom.render(<MyComponent />, document.querySelector('#container'));

// Dispatch an action creator
store.dispatch(wrap(async function(dispatch) {
  dispatch({ type: 'INCREMENT' });

  await new Promise(resolve => setTimeout(resolve, 5000));
  dispatch({ type: 'DECREMENT' });

  await new Promise(resolve => setTimeout(resolve, 5000));
  // Caught by the `wrap()` function and becomes an action with
  // `type: 'ERROR'`
  throw new Error('Oops!');
}));

// Wrap the async function and dispatch an error if an error occurred
function wrap(fn) {
  return function(dispatch) {
    fn(dispatch).catch(error => dispatch({ type: 'ERROR', error }));
  };
}

Moving On

Async/await is still fairly new and the React and Redux ecosystem don't have great support for async functions yet. I'm looking forward to seeing React Suspense when it is finally released. For now, redux-thunk doesn't have a good solution for handling async/await errors beyond wrapper functions or try/catch. This means, as a Redux developer, the responsibility is on you to handle async function errors.

Want to learn how to identify whether your favorite npm modules work with async/await without resorting to Google or Stack Overflow? Chapter 4 of my new ebook, Mastering Async/Await, explains the basic principles for determining whether frameworks like React and Socket.IO support async/await. Get your copy!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus