Asynchronous Redux Actions Using Redux Thunk

June 20, 2018 0 Comments

Asynchronous Redux Actions Using Redux Thunk

 

 

By default actions in Redux are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects. Thankfully though, Redux 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 we’ll introduce the former: Redux Thunk.

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 body of the function once the asynchronous operations have completed.

If you’re curious, a thunk is a concept in programming where a function is used to delay the evaluation/calculation of an operation.

Installation & Setup

First, just add the redux-thunk package to your project:

$ yarn add redux-thunk # or, using npm: 
$ npm install redux-thunk

And now apply the middleware when creating your app’s store using Redux’s applyMiddleware:

index.js

import React from 'react'; 
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk'; import rootReducer from './reducers';
import App from './App'; // 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')
);

Basic Usage

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.

For instance, given the common example of a todo app, 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 done using Redux Thunk. In your container component, the action can be dispatched as usual:

AddTodo.js

import { connect } from 'react-redux'; 
import { addTodo } from '../actions';
import NewTodo from '../components/NewTodo'; const mapDispatchToProps = dispatch => { return { onAddTodo: todo => { dispatch(addTodo(toto)); } };
}; export default connect( null, mapDispatchToProps
)(NewTodo);

The action itself is where things start to get interesting. Here we’ll make use of Axios to send a POST request to the endpoint at https://jsonplaceholder.typicode.com/todos:

actions/index.js

import { ADDTODOSUCCESS, ADDTODOFAILURE, ADDTODOSTARTED, DELETETODO 
} 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
TODOSUCCESS, payload: { ...todo }
}); const addTodoStarted = () => ({ type: ADD
TODOSTARTED
}); const addTodoFailure = error => ({ type: ADD
TODOFAILURE, payload: { error }
});

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

Inside the body of the function we first dispatch an immediate synchronous action to the store to indicate that we’ve started saving the todo with the external API. Then we make the actual POST request to the server using Axios. On a successful response from the server we 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 really external, like JSONPlaceholder in this case, it’s easy to see the actual network delay happening, but if you’re working with a local backend server things may happen too quickly to see properly, so you can add some artificial delay when developing:

actions/index.js (partial)

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)); }); }; 
};

And then to test out error scenarios you can manually throw-in an error:

actions/index.js (partial)

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

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

reducers/todoReducer.js

import { ADDTODOSUCCESS, ADDTODOFAILURE, ADDTODOSTARTED, DELETETODO 
} from '../actions/types'; const initialState = { loading: false, todos: [], error: null
}; export default function todosReducer(state = initialState, action) { switch (action.type) { case ADDTODOSTARTED: return { ...state, loading: true }; case ADDTODOSUCCESS: return { ...state, loading: false, error: null, todos: [...state.todos, action.payload] }; case ADDTODOFAILURE: return { ...state, loading: false, error: action.payload.error }; default: return state; }
}

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:

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 really useful to react differently depending on the current state. For example, if we want to limit our app to only 4 todo items at a time, we could return from the function if the state already contains the maximum amount of todo items:

actions/index.js

export const addTodo = ({ title, userId }) => { return (dispatch, getState) => { const { todos } = getState(); if (todos.length >= 4) return; dispatch(addTodoStarted()); // ... }; 
};

💡 Fun fact: did you know that Redux Thunk is only 14 lines of code? Check out the source here to learn about how a Redux middleware works under the hood.


Tag cloud