Tutorial

Understanding Asynchronous Redux Actions with Redux Thunk

Introduction

By default, Redux’s actions are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects. Redux also allows for middleware that sits between an action being dispatched and the action reaching the reducers.

There are two very popular middleware libraries that allow for side effects and asynchronous actions: Redux Thunk and Redux Saga. In this post, you will explore Redux Thunk.

Thunk is a programming concept where a function is used to delay the evaluation/calculation of an operation.

Redux Thunk is a middleware that lets you call action creators that return a function instead of an action object. That function receives the store’s dispatch method, which is then used to dispatch regular synchronous actions inside the function’s body once the asynchronous operations have been completed.

In this article, you will learn how to add Redux Thunk and how it can fit in a hypothetical Todo application.

Prerequisites

This post assumes you have some basic knowledge of React and Redux. You can refer to this post if you’re getting started with Redux.

This tutorial builds off of a hypothetical Todo application that tracks tasks that need to be accomplished and have been completed. We can presume that create-react-app was used to generate a new React application, and redux, react-redux, and axios have already been installed.

The finer details on how to build a Todo application from scratch are not explained here. It is presented as a conceptual setting for highlighting Redux Thunk.

Adding redux-thunk

First, use the terminal to navigate to the project directory and install the redux-thunk package in your project:

  • npm install redux-thunk@2.3.0

Note: Redux Thunk is only 14 lines of code. Check out the source here to learn about how a Redux middleware works under the hood.

Now apply the middleware when creating your app’s store using Redux’s applyMiddleware. Given a React application with redux and react-redux, your index.js file might look like this:

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import './index.css';
import rootReducer from './reducers';
import App from './App';
import * as serviceWorker from './serviceWorker';

// use applyMiddleware to add the thunk middleware to the store
const store = createStore(rootReducer, applyMiddleware(thunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Now, Redux Thunk is imported and applied in your application.

Using Redux Thunk in a Sample Application

The most common use case for Redux Thunk is for communicating asynchronously with an external API to retrieve or save data. Redux Thunk makes it easy to dispatch actions that follow the lifecycle of a request to an external API.

Creating a new todo item normally involves first dispatching an action to indicate that a todo item creation as started. Then, if the todo item is successfully created and returned by the external server, dispatching another action with the new todo item. In the case where there’s an error and the todo fails to be saved on the server, an action with the error can be dispatched instead.

Let’s see how this would be accomplished using Redux Thunk.

In your container component, import the action and dispatch it:

src/containers/AddTodo.js
import { connect } from 'react-redux';
import { addTodo } from '../actions';
import NewTodo from '../components/NewTodo';

const mapDispatchToProps = dispatch => {
  return {
    onAddTodo: todo => {
      dispatch(addTodo(todo));
    }
  };
};

export default connect(
  null,
  mapDispatchToProps
)(NewTodo);

The action will make use of Axios to send a POST request to the endpoint at JSONPlaceholder (https://jsonplaceholder.typicode.com/todos):

src/actions/index.js
import {
  ADD_TODO_SUCCESS,
  ADD_TODO_FAILURE,
  ADD_TODO_STARTED,
  DELETE_TODO
} from './types';

import axios from 'axios';

export const addTodo = ({ title, userId }) => {
  return dispatch => {
    dispatch(addTodoStarted());

    axios
      .post(`https://jsonplaceholder.typicode.com/todos`, {
        title,
        userId,
        completed: false
      })
      .then(res => {
        dispatch(addTodoSuccess(res.data));
      })
      .catch(err => {
        dispatch(addTodoFailure(err.message));
      });
  };
};

const addTodoSuccess = todo => ({
  type: ADD_TODO_SUCCESS,
  payload: {
    ...todo
  }
});

const addTodoStarted = () => ({
  type: ADD_TODO_STARTED
});

const addTodoFailure = error => ({
  type: ADD_TODO_FAILURE,
  payload: {
    error
  }
});

Notice how the addTodo action creator returns a function instead of the regular action object. That function receives the dispatch method from the store.

Inside the function’s body, you first dispatch an immediate synchronous action to the store to indicate that you’ve started saving the todo with the external API. Then you make the actual POST request to the server using Axios. On a successful response from the server, you dispatch a synchronous success action with the data received from the response, but on a failure response, we dispatch a different synchronous action with the error message.

When using an API that’s external, like JSONPlaceholder in this case, it’s possible to see the actual network delay happening. However, if you’re working with a local backend server, network responses may happen too quickly to experience the network delay an actual user would be experiencing, so you can add some artificial delay when developing:

src/actions/index.js
// ...

export const addTodo = ({ title, userId }) => {
  return dispatch => {
    dispatch(addTodoStarted());

    axios
      .post(ENDPOINT, {
        title,
        userId,
        completed: false
      })
      .then(res => {
        setTimeout(() => {
          dispatch(addTodoSuccess(res.data));
        }, 2500);
      })
      .catch(err => {
        dispatch(addTodoFailure(err.message));
      });
  };
};

// ...

To test out error scenarios, you can manually throw in an error:

src/actions/index.js
// ...

export const addTodo = ({ title, userId }) => {
  return dispatch => {
    dispatch(addTodoStarted());

    axios
      .post(ENDPOINT, {
        title,
        userId,
        completed: false
      })
      .then(res => {
        throw new Error('addToDo error!');
        // dispatch(addTodoSuccess(res.data));
      })
      .catch(err => {
        dispatch(addTodoFailure(err.message));
      });
  };
};

// ...

For completeness, here’s an example of what the todo reducer could look like to handle the full lifecycle of the request:

src/reducers/todosReducer.js
import {
  ADD_TODO_SUCCESS,
  ADD_TODO_FAILURE,
  ADD_TODO_STARTED,
  DELETE_TODO
} from '../actions/types';

const initialState = {
  loading: false,
  todos: [],
  error: null
};

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO_STARTED:
      return {
        ...state,
        loading: true
      };
    case ADD_TODO_SUCCESS:
      return {
        ...state,
        loading: false,
        error: null,
        todos: [...state.todos, action.payload]
      };
    case ADD_TODO_FAILURE:
      return {
        ...state,
        loading: false,
        error: action.payload.error
      };
    default:
      return state;
  }
}

Exploring getState

On top of receiving the dispatch method from the state, the function returned by an asynchronous action creator with Redux Thunk also receives the store’s getState method, so that current store values can be read:

src/actions/index.js
export const addTodo = ({ title, userId }) => {
  return (dispatch, getState) => {
    dispatch(addTodoStarted());

    console.log('current state:', getState());

    // ...
  };
};

With the above, the current state will just be printed out to the console.

For example:

{loading: true, todos: Array(1), error: null}

Using getState can be useful to handle things differently depending on the current state. For example, if you want to limit the app to only four todo items at a time, you could return from the function if the state already contains the maximum amount of todo items:

src/actions/index.js
export const addTodo = ({ title, userId }) => {
  return (dispatch, getState) => {
    const { todos } = getState();

    if (todos.length > 4) return;

    dispatch(addTodoStarted());

    // ...
  };
};

With the above, the app will be limited to four todo items.

Conclusion

In this tutorial, you explored adding Redux Thunk to a React application to allow for dispatching actions asynchronously. This is useful when utilizing a Redux store and relying upon external APIs.

If you’d like to learn more about React, take a look at our How To Code in React.js series, or check out our React topic page for exercises and programming projects.

Creative Commons License