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
- What is Redux Thunk?
- What does Redux Thunk do?
- 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:
-
getUsers: () => { return { type: "GET_USERS", userList: IN_MEMORY_USERLIST } }
-
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:
- On
componentDidMount
we'll dispatchfetchUsers
(a Redux Thunk Action Creator) - The
fetchUsers
Thunk Action Creator will dispatchrequestUsers
then request theuserList
data from an API - Depending on success, or fail, of the API request,
requestUsersSuccess
orrequestUsersError
will be dispatched.
To accomplish this, we'll need five Action Creators. The selectUser
Action Creator is the same as in the previous example.
-
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.
-
requestUsers: () => { return { type: "REQUEST_USERS" } }
-
requestUsersSuccess: (userList) => { return { type: "REQUEST_USERS_SUCCESS", userList } }
-
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.
-
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.