What You'll Learn
- How the Redux data flow works with async data
- How to use Redux middleware for async logic
- Patterns for handling async request state
- Familiarity with using AJAX requests to fetch and update data from a server
- Understanding asynchronous logic in JS, including Promises
In Part 5: UI and React, we saw how to use the React-Redux library to let our React components interact with a Redux store, including calling
useSelector to read Redux state, calling
useDispatch to give us access to the
dispatch function, and wrapping our app in a
<Provider> component to give those hooks access to the store.
So far, all the data we've worked with has been directly inside of our React+Redux client application. However, most real applications need to work with data from a server, by making HTTP API calls to fetch and save items.
In this section, we'll update our todo app to fetch the todos from an API, and add new todos by saving them to the API.
To keep the example project isolated but realistic, the initial project setup already included a fake in-memory REST API for our data (configured using the Mirage.js mock API tool). The API uses
/fakeApi as the base URL for the endpoints, and supports the typical
GET/POST/PUT/DELETE HTTP methods for
/fakeApi/todos. It's defined in
The project also includes a small HTTP API client object that exposes
client.post() methods, similar to popular HTTP libraries like
axios. It's defined in
We'll use the
client object to make HTTP calls to our in-memory fake REST API for this section.
By itself, a Redux store doesn't know anything about async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. Any asynchronicity has to happen outside the store.
Earlier, we said that Redux reducers must never contain "side effects". A "side effect" is any change to state or behavior that can be seen outside of returning a value from a function. Some common kinds of side effects are things like:
- Logging a value to the console
- Saving a file
- Setting an async timer
- Making an AJAX HTTP request
- Modifying some state that exists outside of a function, or mutating arguments to a function
- Generating random numbers or unique random IDs (such as
However, any real app will need to do these kinds of things somewhere. So, if we can't put side effects in reducers, where can we put them?
Redux middleware were designed to enable writing logic that has side effects.
As we said in Part 4, a Redux middleware can do anything when it sees a dispatched action: log something, modify the action, delay the action, make an async call, and more. Also, since middleware form a pipeline around the real
store.dispatch function, this also means that we could actually pass something that isn't a plain action object to
dispatch, as long as a middleware intercepts that value and doesn't let it reach the reducers.
Middleware also have access to
getState. That means you could write some async logic in a middleware, and still have the ability to interact with the Redux store by dispatching actions.
Let's look at a couple examples of how middleware can enable us to write some kind of async logic that interacts with the Redux store.
One possibility is writing a middleware that looks for specific action types, and runs async logic when it sees those actions, like these examples:
Both of the middleware in that last section were very specific and only do one thing. It would be nice if we had a way to write any async logic ahead of time, separate from the middleware itself, and still have access to
getState so that we can interact with the store.
What if we wrote a middleware that let us pass a function to
dispatch, instead of an action object? We could have our middleware check to see if the "action" is actually a function instead, and if it's a function, call the function right away. That would let us write async logic in separate functions, outside of the middleware definition.
Here's what that middleware might look like:
And then we could use that middleware like this:
Again, notice that this "async function middleware" let us pass a function to
dispatch! Inside that function, we were able to write some async logic (an HTTP request), then dispatch a normal action object when the request completed.
So how do middleware and async logic affect the overall data flow of a Redux app?
Just like with a normal action, we first need to handle a user event in the application, such as a click on a button. Then, we call
dispatch(), and pass in something, whether it be a plain action object, a function, or some other value that a middleware can look for.
Once that dispatched value reaches a middleware, it can make an async call, and then dispatch a real action object when the async call completes.
Earlier, we saw a diagram that represents the normal synchronous Redux data flow. When we add async logic to a Redux app, we add an extra step where middleware can run logic like AJAX requests, then dispatch actions. That makes the async data flow look like this:
As it turns out, Redux already has an official version of that "async function middleware", called the Redux "Thunk" middleware. The thunk middleware allows us to write functions that get
getState as arguments. The thunk functions can have any async logic we want inside, and that logic can dispatch actions and read the store state as needed.
Writing async logic as thunk functions allows us to reuse that logic without knowing what Redux store we're using ahead of time.
The Redux thunk middleware is available on NPM as a package called
redux-thunk. We need to install that package to use it in our app:
Once it's installed, we can update the Redux store in our todo app to use that middleware:
Right now our todo entries can only exist in the client's browser. We need a way to load a list of todos from the server when the app starts up.
We'll start by writing a thunk function that makes an AJAX call to our
/fakeApi/todos endpoint to request an array of todo objects, and then dispatch an action containing that array as the payload. Since this is related to the todos feature in general, we'll write the thunk function in the
We only want to make this API call once, when the application loads for the first time. There's a few places we could put this:
- In the
<App>component, in a
- In the
<TodoList>component, in a
- In the
index.jsfile directly, right after we import the store
For now, let's try putting this directly in
If we reload the page, there's no visible change in the UI. However, if we open up the Redux DevTools extension, we should now see that a
'todos/todosLoaded' action was dispatched, and it should contain some todo objects that were generated by our fake server API:
Notice that even though we've dispatched an action, nothing's happening to change the state. We need to handle this action in our todos reducer to have the state updated.
Let's add a case to the reducer to load this data into the store. Since we're fetching the data from the server, we want to completely replace any existing todos, so we can return the
action.payload array to make it be the new todos
Since dispatching an action immediately updates the store, we can also call
getState in the thunk to read the updated state value after we dispatch. For example, we could log the number of total todos to the console before and after dispatching the
We also need to update the server whenever we try to create a new todo item. Instead of dispatching the
'todos/todoAdded' action right away, we should make an API call to the server with the initial data, wait for the server to send back a copy of the newly saved todo item, and then dispatch an action with that todo item.
However, if we start trying to write this logic as a thunk function, we're going to run into a problem: since we're writing the thunk as a separate function in the
todosSlice.js file, the code that makes the API call doesn't know what the new todo text is supposed to be:
We need a way to write one function that accepts
text as its parameter, but then creates the actual thunk function so that it can use the
text value to make the API call. Our outer function should then return the thunk function so that we can pass to
dispatch in our component.
Now we can use this in our
Since we know we're going to immediately pass the thunk function to
dispatch in the
component, we can skip creating the temporary variable. Instead, we can call
saveNewTodo(text), and pass the resulting thunk function straight to
Now the component doesn't actually know that it's even dispatching a thunk function - the
saveNewTodo function is encapsulating what's actually happening. The
<Header> component only knows that it needs to dispatch some value when the user presses enter.
This pattern of writing a function to prepare something that will get passed to
dispatch is called the "action creator" pattern, and we'll talk about that more in the next section.
We can now see the updated
'todos/todoAdded' action being dispatched:
The last thing we need to change here is updating our todos reducer. When we make a POST request to
/fakeApi/todos, the server will return a completely new todo object (including a new ID value). That means our reducer doesn't have to calculate a new ID, or fill out the other fields - it only needs to create a new
state array that includes the new todo item:
And now adding a new todo will work correctly:
Thunk functions can be used for both asynchronous and synchronous logic. Thunks provide a way to write any reusable logic that needs access to
We've now succesfully updated our todo app so that we can fetch a list of todo items and save new todo items, using "thunk" functions to make the AJAX calls to our fake server API.
In the process, we saw how Redux middleware are used to let us make async calls and interact with the store by dispatching actions with after the async calls have completed.
Here's what the current app looks like:
- Redux middleware were designed to enable writing logic that has side effects
- "Side effects" are code that changes state/behavior outside a function, like AJAX calls, modifying function arguments, or generating random values
- Middleware add an extra step to the standard Redux data flow
- Middleware can intercept other values passed to
- Middleware have access to
getState, so they can dispatch more actions as part of async logic
- Middleware can intercept other values passed to
- The Redux "Thunk" middleware lets us pass functions to
- "Thunk" functions let us write async logic ahead of time, without knowing what Redux store is being used
- A Redux thunk function receives
getStateas arguments, and can dispatch actions like "this data was received from an API response"
We've now covered all the core pieces of how to use Redux! You've seen how to:
- Write reducers that update state based on dispatched actions,
- Create and configure a Redux store with a reducer, enhancers, and middleware
- Use middleware to write async logic that dispatches actions
In Part 7: Standard Redux Patterns, we'll look at several code patterns that are typically used by real-world Redux apps to make our code more consistent and scale better as the application grows.