I’m sure you had a similar experience to what I had when I first started using Redux. You checked it out a bit, got excited by it’s elegance, and then asked:
Where do I put async logic like talking to a server?
In this article, we’ll compare two options for handling async logic in Redux: redux-thunk and redux-observable. Redux-saga is another option that shares a lot of similarities with redux-observable, but it’s not included in this article.
A high-level look at asynchrony in Redux
Let’s explore the scenario of talking to a server in the context of a Redux app.
Usually, when you talk to a server, you don’t do it in a fire and forget manner. When you want to send data to the server to save it, you also want to know whether it was successful or not in order to give the user the opportunity to resubmit or take another action. When you want to retrieve data from the server, your request might be successful or result in several different types of failures (expired session, authorization failed, invalid request, broken connection, etc).
For all these different scenarios, you want your Redux store to be updated with the results, allowing your views to… ahem… react to the changes.
But Redux in itself does not give you a good place to put your async logic. With Redux, you dispatch actions from your component and these actions get reduced to make changes to state. If you want to execute asynchronous logic, should it live in the component? Somewhere else with access to the dispatcher?
Fortunately, Redux caters very well for middleware: functions that you can slot into the dispatch pipeline, where your functions have access to the actions being dispatched and potentially dispatch new actions of their own.
This makes it possible for components to stay simple, to still just dispatch actions, but with middleware that intercepts the actions, where it can perform async logic and dispatch further actions to get the results of the async logic reflected on state.
The de facto middleware for executing async logic is redux-thunk, a middleware library created by Dan Abramov, who also created Redux.
With redux-thunk, instead of only publishing plain objects as actions, you can also dispatch functions. These functions are called thunks, and they will be called by the middleware, which will pass
dispatch on the 1st parameter and
getState on the 2nd parameter of the thunk. That way, you can execute async logic at your leisure and use the dispatcher to dispatch downstream actions when you need to.
Redux-thunk in a simple scenario
Have a look at the following simple example of an action creator that returns a thunk. As you can see, it gets the dispatcher through on its parameters so that you can dispatch downstream actions based on the result of calling fetch to get some data from a server. The thunk also gets access to the state on the store, but we’ll explore that a bit later.
The actions that you dispatch can then be reduced state to allow views to display the results to the user.
Redux-thunk is a simple go-to solution for the problem of where to put async logic in a Redux app.
Redux-thunk in a more complex scenario
Often we come across more complex scenarios of interaction, where our app needs to handle a bunch of different events from different sources to derive values on state.
Even though scenarios like this can get really complex, let’s go over a scenario that is still simple enough to digest (hopefully) in an article like this.
Imagine you are working on a web-based collaborative drawing app. In this app, you have access to very granular drawing events, and by drawing a simple curve you end up with about 100 events. This is exaggerated, but I’m making it a high number to make our example a bit clearer.
Let’s say that you want to batch those events up to only send the data up to the server between when the user starts drawing a shape and when the user stops drawing a shape (finger up/ mouse up). Essentially, you want to batch up the events into meaningful units.
When you’re working with redux-thunk, your only construct for handling async logic is a thunk.
A thunk has access to the following inputs:
- The action that was dispatched, retrieved through a closure from the surrounding action creator function
- The current state of the store, retrieved by the
getStatefunction passed through the thunk’s parameter
Now, in this drawing app of yours, an action gets dispatched for each drawing event with
to coordinates. Keep in mind that in our example, drawing a simple curve will result in about 100 events, resulting in 100 actions getting dispatched.
So how will we do the batching using redux-thunk? Let’s first start naively by coding up a thunk that we want to execute for each action. First, we just log out to the console, so that you can see what type of data will be coming through. Note that this time I’ve got a
getState parameter on the thunk, which we can use to access the store‘s current state.
Ok, so the deal is, we want to store these
line events somewhere until we get another action to indicate that the user has stopped drawing (finger up/mouse up).
You might be tempted to think you should just declare a
lines array outside of the thunk, so that you can keep on adding to that until you need to send it through to the server.
There’s a problem with that approach though: a different action (and resulting thunk) will be called to signal the end of the batch when the user lifts her finger/mouse. This means that you will need to share data between these 2 different thunks. You don’t want to share a variable between these 2 actions because it would mean that your thunks are dependent on each other. Which, in turn, would mean that it will be nearly impossible to test properly.
Ok, so how about dispatching actions for each line action so that we roll these values onto the Redux store? You’ll have a
lines array on state, where you’ll maintain all the lines that you want to batch into one call to send it to the server. Once a mouse up/finger up action gets dispatched, you can make a call to the server with all the values from the array and clear out the array on state again.
With this approach, you won’t need to have a thunk for the line drawing action, because you’re just going to reduce it to your state.
So let’s change the code from above to make it a plain-old action object which we reduce to state:
As you can see, now the action that’ll get fired for each line segment (100 per simple curve in our example), will just get reduced into an array, called
lines, to state.
Now you can focus on getting the lines sent to the server when the user lifts her finger or mouse.
This is where you’ll be executing async logic again, so you need to add a thunk this time around, have a look at the
drawingMouseUp action creator below. It returns the thunk that contains the async logic.
The code above now has a new thunk,
drawingMouseUp, which has access to the state through the getState function (provided by the redux-thunk middleware). It uses that to get access to the lines array that it sends to the server.
After sending it to the server, it dispatches an action based on the result of the fetch call.
When it was successful, it dispatches
drawingSyncSuccess , which results in the lines array being cleared out and a drawingSuccess variable to state to be set to true. It also ensures that drawingSyncFailed is set to false.
Inversely, when it failed to sync to the server, it dispatches
drawingSyncFailed, which results in the
drawingSyncFailed value to be set to
true on state, and the
drawingSuccess value to be set to
false. In this scenario, we leave the lines variable alone, because we might want to try sending it to the server again.
The disadvantages of asynchronous composition using redux-thunk
Redux-thunk is quite simple and easy to get started with, but that comes at a cost. Using the approach from above to manage your async composition in Redux has some disadvantages:
- You have to store the lines value in your Redux state store, because you‘re collecting it using one action and sending it up to the server using another. Why is this a problem? We like to think of React components as being functions of state. And in this scenario, we did not need the lines array on state, we just put it there, because we wanted to avoid a variable dependency between 2 different thunks. But with the above approach, we are storing stuff on state, which would call components to be re-rendered, even though they won’t need to. You could use
shouldComponentUpdateoverrides in your components to avoid this particular side effect of storing extra values on state. In any large application, you probably would use
shouldComponentUpdate, but it’s still a smell that your state changes and none of the components are interested in the change.
- Very related to the above issue, it complicates your reducer. When you are storing the
linesarray on state, you need to cater for it in your reducer. As your app grows, your reducers would get more and more complicated as you keep on adding logic for scenarios like this. In a small application this would not be a problem, but would soon overwhelm you when your application starts getting bigger.
- Kicking off the async logic to the server happens in a weird place. You might want to trigger other things from happening when a user lifts her finger/mouse from the drawing component. When you start adding other concerns into this action, possibly more async concerns, your logic becomes difficult to untangle. You want a better way of decoupling concerns that need to be kicked off on a mouse up event from the actual event. Meaning, you would want to embrace an event sourcing over a command pattern. The one approach is reactive, the other is imperative.
Redux-observable is another middleware option that allows you to handle asynchronous logic.
Although it solves the same problem and has some similarity, it works quite differently to redux-thunk.
It embraces the notion that all actions dispatched by your application should be available in an Observable stream and that down-stream actions caused by async logic should be available on an Observable stream.
By default you use RxJS operators on your Observables, but it’s possible to use redux-observable with other Observable libraries.
At a high level, when you use Redux-observable in your application, you need to provide a function with an Observable input and an Observable output. This function is called an epic. The Observable input represents all actions dispatched in the application and the Observable output represents all down-stream actions triggered by your async logic. More succinctly, it calls for actions in, actions out.
Epics are very similar to reducer functions in Redux in that you have one big function and that you can compose it from other more specific functions. That said, where a reducer’s output is recalculated state, the epic’s output is always downstream actions (similar to a thunk).
Let’s look at the simplest example of an epic function using redux-observable. The following is directly from the redux-observable documentation and contains the type annotated signature of an epic.
As you can see, it’s first parameter is an Observable containing all the actions fired in the application and the second is the store, which can be used to get state directly from the store. Thunks also allow you to call
getState to get the latest state from the store, but mostly it’s advised that you try and rely as little as possible on the store when working with epics and try to rely only on the actions being triggered.
Let’s look at an example epic that conforms to this signature. We’re going to take the
loadData thunk that we had earlier and change it to an epic.
Notice that this epic abides to this rule: actions in, actions out. The ofType operator on the
action$ Observable is an operator provided by redux-observable. It makes it convenient to filter actions without having to code up filter functions.
All this epic does is target actions of type
LOAD_DATA, fetch the url on the action, and depending on the promise result output another action which will be dispatched by redux-observable. You could trigger more than one down-stream action too, just as with redux-thunk.
With the code from above, all we have is a different syntax than redux-thunk. But if you look carefully, you might spot how redux-observable solves the issues in the more complex example that we had earlier in the redux-thunk section.
Let’s have a look at the same complex example of a collaborative drawing app where we want to batch up actions (drawing line segments) with other actions (finger/mouse up), but this time using redux-observable.
If you look closely at the code above, you’ll notice that it solves the 3 problems identified when we used redux-thunk to solve the same problem:
- We do not need to store the lines on state anymore, so state just reflects what we want components to use.
- The reducer is simpler now; it does not need to be aware of the lines being batched up anymore.
- We do not have the batching and sending coupled to the
DRAWING_MOUSE_UPaction anymore. It’s only used as a signal to stop batching. We’ve effectively gone from an imperative relationship between the action and batching to a reactive relationship where it becomes a signal.
While redux-observable might look a bit weird to you, it really starts to shine when you plug it into applications with relatively complex async logic.
Keep in mind that you can combine redux-thunk and redux-observable, so you don’t need to fully abandon the one to use the other.
Overall I love the elegance that it brings into your redux app. It makes it much clearer what’s going on and allows you to use a multitude of powerful operators to derive meaning from actions being fired throughout your app.