Sometimes people ask what is the best way to handle asynchronicity in Redux? There is official documentation about it, but I suggest revisiting some basic concepts to see if it’s really that simple.

Redux (right) meets Asynchronicity (left) [Blade Runner 2049 © Warner Bros. Entertainment Inc.]

The basics

A is an object. It’s used as a value somewhere on UI or for its rendering:

An is an object too. It describes an event (or a command) happened in app’s world. By convention it must have the “type” property containing event name and may have some other data:

A is a function. Its signature is

The following example has a function with similar signature and even a comparable method name “reduce”:

In fact, this is exactly what happens in Redux, but instead of an array of numbers Redux gets an infinite array (stream) of events (actions), and its reduction spans the life-time of the app. Of course, and could be primitive types in Redux too, but in real world apps it isn't super useful.

A is all about computation. Nothing more, nothing less. It is synchronous, pure, and simple like a sum.

Developers use Redux through a . It is an object that remembers the computation (reducer) and its first argument (state) freeing you from passing it every time. Interactions are based on calling method to run the computation and accessing the last computed value by calling . Parameter types are irrelevant to because it simply passes them to reducer, doesn't return a value either. This is how a simple Redux store may look and work like:

It looks KISSish and complies with the Single responsibility principle. The example is so simple that it’s hard to imagine where to put asynchronicity into. As you will see later, attempts to add asynchronicity will break some of the definitions written above.

By the way, the original Redux isn't that small. Why? Because it provides various utilities: middlewares, store enhancement, etc. More on this later.

Asynchronicity

If you try to read Redux docs about asynchronicity, the first page you’ll encounter is the Async Actions page. Its title looks rather strange because we know that actions are objects and objects can’t be async. Reading further down you see Async Action Creators and middlewares for them.

Let’s look at what are regular synchronous Action Creators first. From the docs:

Action creators are exactly that — functions that create actions.

A factory function for reducing code duplication in creating action objects, cool. If there’re dispatches of same actions in different parts of the app, Action Creators may help.

Middlewares. They are utilities to override store’s behavior in more functional style (like Decorators in OOP). So, you don’t have to write this by hand if you want to log every dispatched action to the console:

In reality it looks more like a chain of dispatch functions calling each other in order with the original one in the end. But the idea is similar. Async Action Creators require specific middlewares to work, let’s check them out.

Redux Thunk

The first one on the list is redux-thunk. This is how a thunk may look like:

From the description of the library:

Redux Thunk middleware allows you to write action creators that return a function instead of an action.

Returning a function from Action Creators? Actions Creators create actions (objects), it’s obvious from their name. There should be a new term instead.

Google says that by returning functions you may continue to dispatch normally and components will not depend on Action Creators’ implementation. But dispatching “normally” means running the computation of the new state and doing it synchronously. With this new “normal” dispatch you can’t check to see the changes right after the call, so the behavior is different. It’s like patching to allow you to continue “normally” flattening Promises instead of Arrays. Action Creators return objects, so there’s no implementation either. Same time, presentational components don’t usually know about , they operate with available handlers (passed as React props). Buttons are generic. It’s Todo page who decides what a button does, and this decision is specified by passing the right handler.

The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met.

A is a function call, just like . How to delay in JavaScript? By using . How to delay a button click? With , but inside a handler. It is unlikely that patching a button to know how to delay clicks (if it is not a button animating delay countdown, which is different) is necessary. How to call a function if certain conditions are met? By adding an “if-then-else” block inside a handler. Plain JS.

Looking closer at the proposed dispatch call itself. Not only it changes dispatch’s interface:

But we’re passing a function expecting dispatch as an argument into a function called dispatch. This is quite confusing 🤷‍♂️ Melding together different concepts removes simplicity and raises contradictions. But what is the problem that Redux Thunk is trying to solve in the first place?

Adding some async calls turns into:

Nothing has changed for the button, but there is a problem indeed if you have several identical implementations in different parts of the app. Cutting corners with Redux Thunk may look like a solution, but still will add all downsides this middleware introduce. It can be avoided by having only one implementation somewhere on upper level and passing it down or by extracting calls into external functions (basically moving to another file).

Redux Promise

Redux Promise encourages you to dispatch Promises. It is very similar by effect to Redux Thunk, so I’ll skip it.

There is also another way encouraged by subsequent middlewares, but let’s step aside from thunks and asynchronicity for a second and talk about processes happening inside apps.

Business Logic

Apps react on users and environment. Complexity of reactions grows with app’s complexity. Instead of simple things like changing button’s color on a click, apps starts to execute rather complex scenarios. For example, adding a Todo record to the state is simple. Adding it also to the local storage, syncing it to a backend, showing a notification on the screen… is not so. Somewhere between those steps may be even a user interaction.

Flow chart example

Such groups of actions are usually represented by flow charts and have many names: flows, workflows, control flows, business processes, pipelines, scenarios, sagas, epics, etc. I will use the term “workflow”. A simple money transfer between two bank accounts internally may be a huge operation involving distributed transactions between multiple independent parties. But the workflow from the image above may be a simple function:

It looks like and totally is a regular function composition. I made it sync, but it will be the same with promises.

From the point of workflow’s view , syncWithServer(), and Lodash.groupBy() are the same.

Browser APIs, web clients, libraries, triggering UI changes, coming from imports or arriving in arguments, sync or async. They all are just some services that were composed into a workflow to do the job. Even if a workflow is asynchronous, you still run it like this:

If you have a button submitting a Todo, just call it in the event handler. In more advanced scenarios you will have tons of async stuff, cancellation, progress reporting, etc. Achieving this is possible with extended promises, generators, streams, and other libraries and techniques (such as reactive programming).

Workflows exist is many areas of software development, and they aren’t tied to UI state management. They may also call several times with completely different action types or not to have UI indication and state change at all. Workflows may be composable just like functions in JS. Similar concepts exist even high in the clouds and in IoT.

Understanding that workflows are a separate concern is important. By moving business logic into Action Creators this separation starts to vanish. Redux doesn’t require special treatment, nor it is more important than other subsystems in the app.

There two ways of executing workflows: directly and indirectly.

The direct way is the simplest: you call the workflow directly in a handler. This way you have a good visibility of what will happen and control right in the code:

The indirect way is opposite. You start with a dummy action like that must not change any state, but there is another system subscribed to Redux actions. This system will launch a workflow defined for this specific action. This way you may add functionality without updating UI components’ code. But now you have no idea what will happen after a dispatch. Let’s look on the middlewares.

Redux Saga

Redux Saga isn’t really about the Saga pattern.

The Distributed Saga pattern is a pattern for managing failures, where each action has a compensating action for rollback.

It doesn’t help you dealing with state rollbacks. Instead it allows you to write workflows in a CSP-style manner, but with power of generators (which is great). There’re very few mentions of Redux in the docs. 99% of Redux Saga are about sagas themselves hidden in sub-packages.

Sagas are pure workflows, and the docs teach you to manage running tasks, doing effects, and handling errors. The Redux part only defines a middleware which will repost actions to the root saga. Instead of manually building a map [Action → Saga] you need to compose all sagas into a tree similar to reducers composition in Redux. UI code remains the same:

Changes happen only in the corresponding saga:

It is dramatically different to Redux Thunk: the hasn’t changed, Action Creators stay sync and sane, Redux continues to be simple and clear.

Redux Observable

Redux Observable is identical to Redux Sagas, but instead of CSP and Sagas you work with Observables and Epics leveraging RxJS (more difficult, but even more powerful).

Retrospective

So, what is the best way to handle asynchronicity in Redux?

There is no asynchronicity in Redux. You should not build a facade with middlewares like Thunk hiding the real Redux behind it. It couples the knowledge of workflow execution with UI state management and makes the terminology complicated.

There are ways to react on actions in a better manner. You may choose a direct approach of calling workflows manually and/or going by indirect path of binding workflows to actions. Both ways have their own strengths and weaknesses.

Sagas provide a nice balance in ease of use, functionality, testability and may be a good starting point. Same time, choosing Sagas over calling workflows directly is like choosing between Redux and React State: you don’t always need the former.

In advanced scenarios with async modules you may want to register new sagas/epics on demand instead of a prebuilt root saga/epic. But usually it’s better not to overthink.

A /dev/null developer and entrepreneur.