Async Action Helpers in Redux
Rob Cobb, 25 Aug 2017
At Fin, we found ourselves frequently rewriting the same pattern when making http requests from our react-redux app. A few months ago, we extracted the pattern into a suite of async helpers, and ever since, we’ve been using them to replace more and more of our request logic.
In this post, I’ll go over some situations that motivated developing these helpers and show off the helpers themselves. This post assumes familiarity with redux and uses es2015 and es2016 features in the code snippets. Even if you don’t know those technologies, you should be able to follow along fine. The redux docs are a good starting place if you want to learn more.
Single page apps make asynchronous http requests all the time — when loading data or in response to what a user does in the app. Frequently, it’s useful to display an indicator when a request is in flight, and use the data from response to show something in the UI. That means storing state of the request when it’s in flight and storing the api response when the request completes. When a request fails for whatever reason, it’s good practice to handle the failure gracefully, whether that means showing the user an error message, retrying the request, or logging the failure.
Redux doesn’t have strong opinions about managing async requests. The async actions page in the redux docs illustrates one way to implement what we’re talking about, but everyone is free to roll their own. Some action kicks off an http request and dispatches actions before and after the request, so that the request status can be stored (and used e.g. to show a spinner).
The reducer, in turn, needs to respond to each of these actions and update the state appropriately.
If you make lots of async requests in your app, you’ve probably seen several different implementations of this pattern. These vary in subtle ways — different patterns for naming the actions or different store shapes for the request status, the data, and the error. It’s easy to overlook giving proper UI feedback for edge cases, errors, and request state. I’ve seen a lot of after-the-fact prs to add request state handling when someone realizes that they want to show a spinner while a request is in flight.
No matter what request you are making, you want to dispatch an action when you fire off the request, and another action when the request completes. If the request has an error, you want a third action.
Async Action Helpers
To help with some of these issues at Fin, we wrote a set of helpers to make async request handling more uniform. This helped our team remove boilerplate-y reimplementations of the same logic, reason more consistently about code, and reduce the surface area for refactors and updates.
Let’s walk through the steps building up the async helpers. First, a naive snippet that dispatches the right actions around an async request.
There’s lots of currying happening, so let’s break down what’s going on:
- When we call the
createAsyncActionfunction, we get an Action Creator back - so
createAsyncActionis an Action Creator Creator.
promiseCreatoras its arguments.
typeis a redux action type - the kind of action that will get dispatched around the promise.
promiseCreatoris a function that returns a promise - it's how we specify the actual async request to fire.
The helper lets us write our action like:
When the request is fired, redux will get an action with
readyState: 'request'. When the request completes, an action is dispatched with the data from the response and
readyState: 'success'. If the request fails, redux sees an action with
readyState: 'failure' and a payload with the error response.
More sophisticated actions: Args and Thunks
The basic version of createAsyncAction is pretty useful - it can remove a good bit of boilerplate in lots of actionCreators and make sure we don't forget to keep track of the request state.
Still, there are some limitations that make it less than ideal. Most significantly, the Promise Creator isn’t parameterized — it has to make exactly the same request every time. It would be much better if the Promise Creator could take arguments to help shape the request.
You might notice that we are also putting the args into the payload of the action. Having access to those args in the action has proven to be useful in reducers (which object should we update since we are making a request?) and when debugging (what params was this actionCreator called with?)
While we’re at it, it would be great if we had the flexibility to write Promise Creators as thunk actions, so that they had access to dispatch and getState too.
Now, we can write async actions with as much complexity as we could want, and still ensure that the right actions will be dispatched around the request.
Now, we can write sophisticated request actions with confidence that the right actions will be dispatched around them.
What about the other half of the pattern, managing the data? Without any helpers, we end up implementing a reducer that responds to each of the actions that gets dispatched from the function our createAsyncAction helper returns.
As you might have guessed by this point, we have a helper function to reduce this boilerplate too!
What does it do and how does it work? Like
createAsyncReducer function is a higher-order function - it takes a type and returns a reducer that updates the loading, data, and error keys when actions of that type are dispatched.
loadingkey is a boolean that returns
truewhen the action is the initial request (
readyState: 'request') and
falseif it is
errorkey keeps track of the errors from the action - it's useful for displaying an error message from api.
datakey naively returns the data from the action. For a wide set of simple requests, this is actually all we need.
The naive reducer returned by the simple
createAsyncReducer function above is pretty useful for simple data fetches, but we often want to do more than just store the data from a request.
createAsyncReducer function we use supports swapping in reducers that can respond to different actions or handle the request actions differently.
Now, if we want to use some of the
createAsyncReducer features but override or add some keys, we can. Of course, we can always write the full reducer out without using the helper in cases where we want a different store shape or we aren't getting any advantage from the asyncReducer.
To Wrap up
Using these helpers, implementing a new fetch action is as simple as:
We reduce a lot of boilerplate without losing flexibility. Hopefully these patterns are useful for writing your own async redux code. The package of helpers is on github and can be installed through npm.
If you are interested in similar libraries that do this kind of thing for you, you might be interested in:
Some of the improvements to the async helpers that we’ve thought about, but haven’t built yet:
- update async reducer to listen to multiple actions
- update async action and reducer to keep in-flight requests in the store (for canceling / updating)
- update async reducer to filter actions before passing them to the combined reducer
Bonus: Async Observable
We at Fin also use redux-observable to respond to streams of actions, and sometimes we initiate http requests from our epics. We’ve got a parallel helper to use in our redux-observable epics to make sure that we get the same bookending of requests with redux actions.