Tutorial

Understanding Asynchronous Redux Actions with Redux Thunk

Updated on October 9, 2020
    authorauthor

    Alligator.io and Bradley Kouchi

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

    1. 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.

    Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

    Learn more about our products

    About the authors
    Default avatar
    Alligator.io

    author



    Still looking for an answer?

    Ask a questionSearch for more help

    Was this helpful?
     
    3 Comments
    

    This textbox defaults to using Markdown to format your answer.

    You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

    A very well structured article. Cleared many doubts. Thanks!

    OMG. It is soooooo helpful article . To this day I have struggled to understand async actions . After reading this article I’m 100 percent understand how to create asynchronously actions . Thanks so much … Good luck !!!

    This comment has been deleted

      Try DigitalOcean for free

      Click below to sign up and get $200 of credit to try our products over 60 days!

      Sign up

      Join the Tech Talk
      Success! Thank you! Please check your email for further details.

      Please complete your information!

      Become a contributor for community

      Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

      DigitalOcean Documentation

      Full documentation for every DigitalOcean product.

      Resources for startups and SMBs

      The Wave has everything you need to know about building a business, from raising funding to marketing your product.

      Get our newsletter

      Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

      New accounts only. By submitting your email you agree to our Privacy Policy

      The developer cloud

      Scale up as you grow — whether you're running one virtual machine or ten thousand.

      Get started for free

      Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

      *This promotional offer applies to new accounts only.