Redux may not be fashionable right now, but when I need to show React off to new developers, I usually start by showing them Redux dev tools (“I can see the state of my app while it’s running! Check this out, time travel debugging! Cool, huh?”)
Redux makes synchronous state updates explicit and reasonable, and makes visible the contract between React and the application: UI is a function of state, and it’s for this reason I’ll continue to use it.
But asynchronous effects are a whole other matter. I have variously used the middleware libraries Redux Thunk, Redux Loop and Redux Saga. Part-way through reading the Redux Observable documentation I started a clickbait Reddit thread and got pointed towards a better approach.
Before I go into that approach, here’s where I see problems with the leading async middleware libraries:
The problem with thunks
Using Redux Thunk mean having some of your action creators return async functions (thunks) that have access to
dispatch(), instead of plain objects.
Consider a typical async scenario — you want to grab something from an API and dispatch the response to the store. But you may also want to do other things with that API response — pop up a notification, say, or record some analytics to a 3rd party API.
If you incorporate those effects into your thunk, it will have some undesirable characteristics:
- it is now tightly bound to areas of your state it was previously unconcerned with
- your previously simple function is more bloated
- some of your async business logic, like analytics, gets spaghettified throughout unrelated thunks
- previously pure-object-returning actions now have to become thunks just to fire off async side effects
Not great. The problem with thunks is that they’re not powerful enough, not on their own.
The problem with sagas
Redux Saga allows you to define generator functions (sagas) that grab actions as they enter the store, perform whatever asynchronous actions they need, and
dispatch() resulting actions. They can also
select from the store.
This removes most of the problems exhibited above. By creating a
showNotificationAfterApiResponse saga that grabs the response action and updates some UI state, and a separate
analyticsSaga that grabs disparate actions to record events to your 3rd party analytics API, you can keep your original fetch-and-dispatch effect untouched.
However, sagas can do more than that. Being based around generators, they can pause their execution until certain conditions are met. They can also grab actions under certain conditions (just one action ever, or just the latest action, or take every action always). And this means sagas exhibit some different undesirable characteristics:
- Generators are inherently stateful, so the effects that will be executed by your application do not solely depend on Redux state+action, but on the current state of this saga and any other saga that may get called by its operation
- Generators are imperative, meaning anything other than the most simple cases can be challenging to grok
- Generators are quite unusual syntactically, increasing the barrier to entry for new developers
In search of a better approach
In my frustration, I sought out Redux Loop, a port of Elm’s effect system. It expanded on the Redux formula(state+action=new state) to include asynchronous side effects (state+action=new state+side effects). The library is avoided by a lot of developers as it seems to affect the purity of Redux reducers, but that’s a bit misleading: it doesn’t actually run the effects, it just returns what they will be.
As it turned out, I abandoned Redux Loop for other reasons — it does not play nice with other middleware (such as the wonderful Redux Persist), its syntax is fairly clunky, its uptake is low and falling — but the looped effect code had some properties that I found attractive:
- It could take actions like sagas or epics without the async pollution experienced by thunks
- It colocated async and synchronous effects, making it clear what responsibility each slice had for the side effects that were run
- By keeping the
switch (action.type)mechanic, it resembled vanilla Redux, and so was more grokkable than generators or RxJS streams
“Why not write your own custom middleware?"
This was the sentence I read that forever changed the way I wrote async Redux. I hope to use it to convince you to change yours, too.
Vanilla Redux consists of 3 elements of boilerplate for each slice of state:
- Action types
- Action creators
My approach adds a fourth:
- Custom middleware
Not every slice needs all four, though I find most do. Some slices just need the vanilla synchronous 3 elements, some (like analytics) only need the middleware element, and I have had exotic cases that didn’t need state or a reducer, but just actions and the middleware to dispatch them.
In all cases, the pattern I use for custom middleware is this: alongside any
userActions and a
userReducer, I’ll add a
Note how, when actions go in the top of the async function, they immediately get
next(action)-ed (passing them along to the next middleware or the destination store), and then there’s a
switch (action.type) with
cases that perform the side effects before the promised result is resolved (which allows your
dispatch()es to be
And this is what I love about this pattern: the asynchronous middleware closely resembles a synchronous reducer. Just as the reducer is pure in that a given action on a given state will always produces the same resultant state, the middleware has its own purity: a given action on a given state will always run the same effects.
This makes async effects easy to understand. It has given up the power of the saga (it now can only take every action of a type) but in doing so has simplified it and made tracking your side effects obvious.
So there it is- don’t use async Redux middleware: write your own async Redux middleware instead.
An epilogue — and a defence of sagas and epics
The truth is that the custom middleware I now write strongly resembles the sagas that I used to write. If a team using Redux Saga is suitably disciplined then they can sidestep the foot gun that is generator state.
Redux Observable via RxJS shares many of the benefits of Redux Saga but has a Functional Programming declarative style, piping of a stream of actions through its epics: very fashionable. If you are comfortable with point free style, it’ll have a much lower barrier to entry than it does for me. Once I’ve lost the last of my imperative mindset, I may well give it a real go.
Until then, I’ll stick with my custom middleware. For something new, it looks and reads like a familiar friend.