Are you a front-end developer? Are you working on a modern single page application? Are you using React? Chances are you are you using Redux. Even if React isn’t your thing, chances still are part of your single page application state is being kept on a Redux store.
One of the reasons for a library that simple to get that popular that fast is its disruptive nature. It first got around among the whole ’flux architecture storm’ when every day we got a new library ready to implement the abstraction in a whole new, more practical and better way.
It basically settled the storm by providing a minimal implementation of the minimal concept enforcing a set of rules that took away some power of representation from the developer and turned it into predictability.
Redux came with a set of rules to take away some of this power so we could avoid the regular self-sabotaging we seem to gravitate towards.
Seeing the benefits this brought us, we soon realized Redux wasn’t taking enough power. How about my
fetch calls? And my
timeouts that are running like crazy and I can’t keep tabs because Redux doesn’t track side effects? How about testing without writing to a database?
Redux was agnostic about one crucial type of work any application does: side effects. From the big ones like fetching data, to the ones people usually forget like delaying code execution for some time or even generating a random number.
Our application state isn’t really predictable if the side effects that affect it aren’t properly managed.
Well, if the library itself didn’t care, the open source community certainly did. With the previous flux storm settled, we’d enter in a new one about Redux side effect management: each middleware library was doing the side effect management its own way and it’s up to you to figure out once again what’s the best approach. Fatigue, anyone?
Yeah, but I read a lot about some of the most popular ones and I wanted to bring my opinions here. Hopefully, people can read this article when deciding which middleware to use so the fatigue is even slightly less fatiguing.
Our contestants for the day are:
- redux-thunk, weighing 6500 stars
- redux-saga, weighing 9900 stars
- redux-effects, weighing 450 stars
- redux-loop, weighing 1300 stars
These were both popular and different enough from each other so that it’s worth to talk about. If you just want the one I prefer at the moment, I’ll save you the time: it’s loop. If you want the reasoning, keep reading.
I’ll code a minimal example of what a
fetch call for the cat image that starts this post would look like with each library. I know we don’t use
fetch for simply getting images, but I don’t care. I’m sure you’ll be able to extrapolate the example and besides, did you see how cute it looks?
This is about as popular as a library can get under 15 lines of code.
This ridiculously simple and obvious middleware was created by Abramov himself, which is the guy that brought us Redux. The idea here was to show how powerful middleware could be and how a side effect could be run with the store without making the reducer impure.
What it does is: when you dispatch a function (thunk), the middleware calls it with the store as a parameter. That’s it. It enables us to run side effects because the function gets called and the action creator never had to be pure. The store is passed, so all the state is available and other actions can be dispatched. So… we’re done, right? Just use redux thunk.
When people actually started using it as the default side effect middleware for redux we started seeing the cracks. How to test the action creator that returns the function without running the side effect itself? That was its Achilles heel. Thunks aren’t easily testable and we’d be insane to run untested code in production.
I’d go further and say the real problem of thunks is more elegantly put as ’they are too powerful’. The store is passed to a function that is called when dispatched. What are the new rules here, despite having to dispatch the function? If you just called it by hand, wouldn’t it be the same thing?
We have to let go of some power in order to get more control.
I honestly wouldn’t recommend thunk to anyone. I don’t see the use case. If you were to use thunks, just use nothing at all. The benefits provided by thunks just aren’t enough to justify its costs, in my opinion.
This is where things start to get interesting. The middleware brings the previously established concept of sagas to Redux. What it does is: it uses generators in a channel-like abstraction. Each saga is a generator that will generate side effects. The side effects are gonna be run by the middleware and the result will be passed back into the generator.
So instead of running
fetch, you’ll ask the middleware to run it for you. Instead of listening to the store for a given action, you’ll ask the middleware to listen for you. The middleware will do the dirty work so you can work with only pure functions (or generators for that matter).
Where thunks fall apart, sagas prevail: testing the sagas is nothing more than seeing if they ask the right questions (or yield the right effects) while you provide mocked answers (or inject data into the generator). You test where the middleware would run the effects.
So, sagas are great. I don’t have many complaints. It does the work, it does it well, it’s transparent and testable. The complaints I do have are: they are hard to get into and they still retain too much power.
For the first point, I’ll just say this: try suggesting using sagas to anyone in your team right when they just figure out how promises work. See if they'll like to have to learn a whole new abstraction (that is much more complex than redux) to deal with a problem that isn’t that clear to them.
For the second one, I think it’s quite clear. Sagas are just as powerful as thunks. Maybe even more. It’s not unusual for me to see a huge pipeline of work including fetching, sleeping and user interaction in a single saga. God help the guy that’s tasked with testing it.
Too cool to be supported.
This one is quite clearly not an option for anyone starting a new project. It’s not been updated in a year and most of the required plugins are broken or at least heavily outdated. Why am I writing about it then?
The idea is neat, I like it. I think it’s a step in the right direction and worth mentioning. It starts by not trying to change anything about Redux. You heard me: there are no additional abstractions to learn from the library. Moreover, it takes much of the power away formalizing what side effects can be run and how they can be run by the user.
Here’s how it works: a middleware listens for effect actions. These special actions have a specific interface and know how to identify themselves, so they can be run by another middleware resolving to an action that will then be dispatched to the store. That’s it.
If you skip the middleware, everything still works, the effects and subsequent resulting actions just won’t be run. The power contained in the effects are also limited: they can only run a Promise and dispatch a resulting action, no more. It is easily testable and easily plugged into any project.
So what did this middleware did wrong? Why is it unpopular and even kind of deprecated? Well, for starters people don’t like their power taken away, as I already stated. Secondly, the middleware creator made a really bad decision that would make this library hard to support and adopt: the effects are also middlewares that need to be implemented and supported.
That means if you want to use this middleware, you also need an extra middleware for each kind of effect you want to be run through the store. This makes the middleware developers responsible for something they really shouldn’t be, like providing a fetch function.
As an example, I tried to plug this into a recent new React Native project only to realize the fetch middleware was using a fetch version incompatible with the react native environment, so I had to drop it altogether.
As a final nail in its coffin, this middleware encourages the user to engage with an arguable anti-pattern behavior. Getting data from the store in action creators should be done sparingly. If your side effects need to be defined in action creation time, you will most likely have to access the store to build the effect payload, so yeah, you’ll be doing a lot of
getState in action creators.
About as impure as a reducer can look, without being impure.
So, how it works? It changes the reducer signature. It now reduces the state action pair to a new state and an effect. The effect is an object description of what needs to be run and which actions will be dispatched when it does. The middleware takes upon itself to properly run the effect and dispatch underlying actions.
It’s testable because the reducer returns the effect object description, just like sagas. It doesn’t need extra middleware because the effect creator takes a function that represents the effect, so you can use your already coded fetch requests. It doesn’t encourage the state selection on action creator anti-pattern, because the effect needs to be reduced from actual state, so you got all your data in there already. It is as powerful as the redux-effects solution I’d say, in the sense that you can code an effect that dispatches an action and that’s about it.
So what’s bad about it? Well, I just found it and started using it a few weeks ago. I promise I'll tell you when I find out, but for now, this is my side effect middleware of choice.
Oh, the API could be better. It’s too verbose I think. That’s kind of nitpicky, right?
That’s all folks
Let’s see which library stands tall when the dust settles. Some might say sagas already won, but I’ll place my bet in loops for the time being.
Thanks for reading this far and always remember:
Keep your actions close and effects even closer.