A simpler way to handle async in Redux

… or how to let actions be plain objects without going all in with sagas or reactive programming.

Kjell-Morten
9 min readFeb 4, 2018

I simply love Redux. Why? It cleans up my thinking around state and flow in a front-end application. I’ve found a work flow where I develop the logic of a web app with the same tools that I use to develop for back-end — based around writing tests (I love AVA too) — and the parts that are specific for web apps are reduced to displaying the state in the right way* with React or something similar.

The Redux dispatch method.

The strength of Redux comes from strong, functional concepts, based on immutable objects and pure functions with no side-effects. But this is also why it might give you some headaches. Most web apps need to communicate with a back-end, one or more data sources, or what have you; and fetching data through an asynchronous request is by definition a side-effect. A pure function should return the same result given the same input, every time, and the result of a function that returns data from a different source will be dependent on that source as well — not only the function’s arguments.

Everything in Redux is done by dispatching an action — as a plain object — which is passed on to a reducer function and used to create a new state object to render the application from. This pattern fits well with getting data from another source, where a dispatched action would send a request to e.g. a REST API and then create a new state object with the data it returns. The only gotcha is that it is not possible within the pure functional approach of Redux. Instead it is is done by adding middleware to Redux that intercepts actions and performs async calls “on top” of Redux.

I’ll describe three different approaches to this “interception”, and suggest a fourth. All four approaches come with their own upside, but in my mind the fourth approach is the simplest, as it allows actions to stay plain objects without requiring you to learn a new framework or programming technique.

In every case, I’ll show how the approach may be used to fetch user data from GitHub and dispatch this data with a DID_FETCH_USER_DATA action.

Thunks: Dispatch functions instead of actions

When you set up Redux with the Thunk middleware, you can suddenly dispatch functions — called thunks — in addition to action objects. Redux wouldn’t know what to do with a function, but the middleware picks up any dispatched functions, and instead of passing them on to the reducer, it calls the function, with the dispatch method as argument. The thunk may do all the async operation it likes, and then dispatch regular action objects with the retrieved data. These actions will be passed on to Redux’ reducer and end up as a new state object.

A very simple thunk, that retrieves user data from GitHub:

A simple thunk. Remember that a real world thunk should have error handling etc.

A thunk is usually written as an action creator that returns the function to dispatch, as in the example above. You would typically call this with dispatch(fetchUserThunk('kjellmorten')).

There’s also a related approach, called redux-promise, where, instead of dispatching functions, you dispatch an action with a promise as the payload. This is a middleware too, and will pick up actions with a promise, wait for the promise, and then dispatch a copy of the action — this time with the resolved or rejected value as the payload. This might be simpler than writing a thunk, but the trade off is that you don’t have the same control over what you dispatch.

Sagas: Describe async behavior with objects

While thunks solves the “async problem” of Redux by inserting async functions into it, sagas represent a different approach that aims to solve asynchronicity with kind of the same philosophy that Redux was built on — working with immutable objects and pure functions.

With sagas, you will only ever dispatch regular action objects. Instead you set up redux-saga as a middleware that will listen in on the actions and trigger side-effects when appropriate.

Furthermore, these side-effects are also defined as objects. A “saga” is essentially a function that returns objects describing asynchronous operations, and the middleware knows how to “execute” these objects. This is much easier to test, as you don’t have to mock the fetch api to make sure it is called in the right way — you only test that your functions return the expected objects. With Redux and saga, everything is an object.

Here’s the example of getting user data from GitHub, written as a saga:

A simple saga. Again, remember to include error handling etc in a real world case.

The code might look simple enough, but if you look closer, there’s actually a lot going on here. I’ll not go into details, but keep the focus on the principles.

First, the functions with asterisks are called generators. If you haven’t seen them before, just think of them as functions that may return several times. For each yield a value is returned to the caller, which again may return a value back to the function, and it resumes until the next yield.

call and put are convenience functions that create saga objects, so you don’t have to remember the exact object structures. call creates a saga that calls a function. The first argument to call is the function and the rest of the arguments are used as arguments to that function. In the example above, fetch will be called with the provided url. In the same way, put will create a saga object that dispatches the given action.

Finally, the watchActions generator will be handed to the saga middleware on initiation, and essentially tells the middleware to pass every FETCH_USER_DATA action to fetchUserSaga. Once set up, you dispatch plain object actions, and user data will be fetched asynchronously behind the scene in response to the relevant actions.

This all may sound a bit convoluted, and in a way it is, but you’re left with a clear way of defining async operations as objects, which is easier to test, and a lot of implementation details are left to the saga middleware.

Observable: Turn your actions into a stream

Okay, let’s follow that train of thoughts and take it up a notch.

There’s a coding pattern where you set up observer functions that will be called when there’s changes in an object. We call this kind of object an observable. Put this together with reactive programming, where you declare what should be done in the event that it happens, and you get a coding style that actually resembles how you would work on an array in functional programming — but instead of an array, you’re operating on events spread out in time.

Come again? Well, okay. Picture this is as if you had recorded all the events in an application over time, put them in an array, and were then going to write the code to run through the array and respond to each event. Except that you will write that code up front, and the events are handled as they happen.

What does this have to do with Redux and async operations? Well, the actions dispatched throughout the lifetime of an application might be viewed as a stream of events. So treating them as observables, and applying reactive programming to define what to do with this stream, actually makes a lot of sense. And this is exactly what redux-observable lets you do.

Let’s see what our example of getting user data from GitHub would look like with reactive programming:

A simple epic. And … real world epics should have error handling etc.

Not too shabby. If you know some functional programming concepts and pictures the stream of actions as an array, you might actually understand this right away. But let’s go through it.

First, we filter the actions, so that we only let those of type FETCH_USER_DATA through. This is actually so common that redux-observable has a convenience operator. Instead of the filter operator, you may use ofType with the action type as only argument.

mergeMap will act on each item in the stream, and merge the results back to the stream. The merging involves getting the value from other observables or promises, and the latter is handy in this case, as every action that passes our filter will result in a call to fetch and return a promise.

Finally, the map operator also acts on each event, but without any merging. This is fine here, as the stream after the mergeMap consists of data objects from fetch. Each data object is set as the payload on a DID_FETCH_USER_DATA action. Now, we have a stream of actions, and the redux-observable middleware will dispatch these actions for us.

This allows for a different way of reacting to actions, which becomes only more powerful the more you use it. A stream may be filtered, mapped, and delayed, etc.

You may also note on a higher level, the saga and observable approaches are a bit similar, in that none of them will alter the built in dispatching of actions in Redux, but instead let you write small functions that are called when certain actions are dispatched. How you write these functions are, on other hand, rather different.

A fourth approach: Side-effects in action handlers

The three approaches so far span from the simple one, where you directly dispatch a function or a promise to execute async operations, to the more complex — but also more powerful — where actions are only dispatched as objects, but you have middleware that kicks off async operation based on the actions that passes through it.

They all have pros and cons, and may be right for different use cases, but there seems to be a quick jump from the simple to the more complex. What if you want to dispatch actions as simple objects, but don’t want to get your head around sagas or reactive programming?

Enter action handlers.

I have used this with success in some simpler applications, and as long as you’re staying organized, it should work well in larger applications as well.

The idea is to write straight forward functions to perform async operations, just like the functions you would dispatch as thunks, but instead of dispatching them, you register them as handlers for certain types of actions, like you do with sagas and observables. Dispatching an action of the right type, will trigger the async operation without disturbing the side-effect free flow of actions.

To register action handlers, use redux-handler-middleware, written by Zach Perkitny, which takes an array of objects with action types and corresponding handler functions, and returns a Redux middleware that you may apply when you create a new Redux store.

A handler will get an object with getStore and dispatch as its first argument, and the action as the second.

Our GitHub user data example could look like this:

This is not the shortest example, but in my view, it is the example that requires you to understand the least amount of new concepts. You should definitely learn reactive programming, if you’re not familiar with it already, but it might not be necessary for doing async operations in a simple web application.

The example should not need any explanation now that we have gone through all the other examples, but note that I register the handler function on the afterHandler. The means that the handler will be called after the reducer in Redux — the state is updated before our action handler is called.

There is a beforeHandler as well, but it’s usually better to update the state before you kick off any side-effects. For instance, the FETCH_USER_DATA action could change to state to fetching: true, to display a spinner while we wait on the async request. Using the afterHandler may also limit the effects of any delays in handler functions.

Finally, a word on action types. I have landed on a convention where I name the action that will trigger an operation with a verb and something descriptive, like FETCH_USER_DATA. When the async operation is successful, I dispatch an DID_FETCH_USER_DATA action with the results, and in the case of an error, a DIDNT_FETCH_USER_DATA action. This works well with the action handler approach.

Another convention you could adopt, is to use the same action type for all actions, but set a status property on the action to signal if it’s a success or an error, like redux-promise does. Some will also argue that actions should not be named like commands, but as events, describing what has happend and living the concequences to other parts of the code. That will still work with the action handler approach, you just have stick with your convention.

So, there you have it. Four approaches to async operations that works with Redux — or actually five, if you count redux-promise as a separate approach. It’s still up to you to figure out which one is the best fit for your case.

*) I started the article by talking about the work that happens on the front-end as simply a matter of displaying the state in the right way. That’s a huge oversimplification if you take it as an attempt to throw out the design disciplines, the content strategy, the user research, and all UX considerations that goes into building a successful application. But that is not my point at all.

I have worked most of my career in the fields of design and UX, and know very well the value and necessity of these disciplines, but when you know what you’re building, thinking of the front-end work as a matter of displaying the state might not only make your life a lot easier, it may even discover issues with the UX that did not come up during the design activities.

More about that another time. :)

--

--

Kjell-Morten

Coder, UXer, and founder. Currently working with integration services for the JavaScript world.