Asynchronous Actions with Redux Thunk

If you've been working with React and Redux you're undoubtly familiar with Actions - used to send information from your application to the state. By default, Redux Actions are synchronous - for every Action dispatched, the state is immediately updated.

Modern web applications, however, often involve asynchronous events, most commonly in the form of network requests like API calls. In these cases the Actions can't immediately update the application's state because the request is asynchronous. In other words, the application's state can't be updated until the request returns (or fails) at some unknown point in the future. This means our Actions need to be Asynchronous, instead of Synchronous.

With Asynchronous Actions common in many React/Redux applications there are several React/Redux packages to aid in the process - Redux Saga, Redux Promise, and Redux Thunk. In this article we'll take a look at the last of these options - Redux Thunk.

What We'll Cover

  1. What is Redux Thunk?
  2. What does Redux Thunk do?
  3. How to use Redux Thunk

Let's Start, With Synchronous Actions First

Before we dive into Redux Thunk, let's get some context by taking a look at a standard React/Redux application with Synchronous Actions first. We'll start with a simple application:

  • A user can view a dropdown of contacts
  • A user can select a contact to view their details

From the React point of view, we'll have 3 components:

  • <App /> - a Redux-connected container component
  • <Select /> - a <select> element, with each user as an option
  • <CurrentUser /> - to display the selected contact's details

When the component mounts, a GET_USERS action is dispatched to update the state with the array of users (stored in an in-memory array). And when the <select> is changed, the SELECT_USER action is dispatched to update the state with the currentUser. The current user is then displayed.

To accomplish this, we'll need two Action Creators:

  1. getUsers: () => {
      return {
        type: "GET_USERS",
        userList: IN_MEMORY_USERLIST
      }
    }
    
  2. selectUser: (user) => {
      return {
        type: "SELECT_USER",
        currentUser: user
      }
    }
    

As with all standard Redux Action Creators, both return plain JavaScript objects.

I won't go into the Reducers here because they're relatively simple updates to the state. The demo has the full code.

The Reducer for the GET_USERS Action updates the state with the users array:

return {
  ...state,
  userList: action.usersList 
}

And the Reducer for the SELECT_USER Action updates the state with the selected user:

return {
  ...state,
  currentUser: state.userList.find(user => user.id == action.currentUser)
}

Here's our synchronous app:

See the Pen React - Redux - Synchronous by Brett DeWoody (@brettdewoody) on CodePen.

Take a look at the Codepen to ensure you're familiar with each part before continuing...

Going Asynchronous

The app above is relatively simple because userList is coming from an in-memory array. But in a production application it's likely userList would be retrieved from an API - requiring an asynchronous request.

This creates a problem, because in standard Redux, Actions are synchronous. So how do we handle Asynchronous Actions in Redux?

As you probably expected, the answer is Redux Thunk.

What Is Redux Thunk?

Redux Thunk is a Redux middleware package for React Redux that enables Asynchronous Actions. In case you're not familiar with Redux middleware - Redux allows third-party packages to hook into the point between dispatching an action and the reducer, allowing us to run code for every action/reducer.

One method I find helpful when diving into a new library is to view the source code, so here it is:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

And that's it. The entirety of the Redux Thunk package. Beautifully simple.

So what are these few lines of code doing and how do they help with asynchronous actions?

What Does Redux Think Do?

If you look at the Synchronous Actions examble above, you'll remember every Action Creator MUST return an action (a plain JavaScript object), containing a type property, and payload.

But with Redux Thunk, Action Creators can return a standard Action object, OR a function. If a function is returned, the function is executed by the Redux Thunk middleware. Even better, the "function doesn't need to be pure; it is thus allowed to have side effects, including executing asynchronous API calls. The function can also dispatch Actions."

How To Use Redux Thunk

To use Redux Thunk it first needs to be installed. If you're using npm you'll need to install the redux-thunk package, then import redux-thunk into and use Redux's applyMiddleware method to apply it to your store.

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

const store = createStore(
  rootReducer,
  applyMiddleware(reduxThunk)
);

Now back to the app's dataflow - let's think through how this would be used in our example Redux app from above:

  1. On componentDidMount we'll dispatch fetchUsers (a Redux Thunk Action Creator)
  2. The fetchUsers Thunk Action Creator will dispatch requestUsers then request the userList data from an API
  3. Depending on success, or fail, of the API request, requestUsersSuccess or requestUsersError will be dispatched.

To accomplish this, we'll need five Action Creators. The selectUser Action Creator is the same as in the previous example.

  1. selectUser: (currentUser) => {
      return {
        type: "SELECT_USER",
        currentUser
      }
    }
    

Then we'll add three Action Creators for the various stages of our asynchronous request - on request, on success, and on failure.

  1. requestUsers: () => {
      return {
        type: "REQUEST_USERS"
      }
    }
    
  2. requestUsersSuccess: (userList) => {
      return {
        type: "REQUEST_USERS_SUCCESS",
        userList
      }
    }
    
  3. requestUsersError: (error) => {
      return {
        type: "REQUEST_USERS_ERROR",
        userList: [],
        error
      }
    }
    

The final Action Creator, fetchUsers() is what's known as a Redux Thunk Action Creator, because instead of returning a plain JavaScript object, it returns a function.

  1. fetchUsers: () => {
      return function (dispatch) {
        dispatch(actionCreators.requestUsers());
    
        return fetch("https://jsonplaceholder.typicode.com/users")
        .then(
          response => {
            if (response.ok) {
              return response.json()
            }
            throw new Error("404");
          }
        ) 
        .then(json =>
          dispatch(actionCreators.requestUsersSuccess(json))
        ).catch(error => {
          console.error(error);
          dispatch(actionCreators.requestUsersError(error))
        })
      }
    }  
    

The bulk of the work is done in this fetchUsers Action Creator. First, it dispatches the requestUsers action, to indicate our API request has been initiated. This is useful for updating a loading state to show a loading icon or similar.

Then fetchUsers uses fetch to request the userList from the API and returns Promise. Depending on the success/failure of the request the Promise will either resolve and dispatch requestUsersSuccess, or be rejected and dispatch requestUsersError.

In the case of requestUsersSuccess we'll update the state with the userList returned from the API. Or in the case of requestUsersError we'll update the state with an error which we can display to the user.

Here's the full app using Redux Thunk:

See the Pen React - Redux - Thunk - Asynchronous by Brett DeWoody (@brettdewoody) on CodePen.

Redux Thunk, while simple, is also extremely powerful. Hopefully this demo helps shows how simple it is to implement.

Comments