Redux side effects and you

Spiral by Khairul Nizam, CC BY 2.0

Should you care about alternatives to the venerable redux-thunk?

It seems every couple of days now, someone comes up with a solution for side effects in Redux to replace redux-thunk. Thunk is the go-to middleware that allows us to return functions from action creators to dispatch action objects synchronously or asynchronously. If you’ve never used it or don’t know what I’m talking about, the official documentation describes it pretty well.

Some of those alternatives have been around for a while now: redux-sagas, redux-loop are gaining visibility, while other models such as actors are also coming in the spotlight every now and then.

But why does it matter, if at all? Are they worth considering?

Edit 3/27/2016: Highly related to this post, see the following discussion on the Redux github repo about side effects handling

If you’re happy with redux-thunk, stop there

It’s pretty much that simple.

If you’ve never heard of those alternatives, or if you’re wondering why you should care, you can stop worrying about it. Thunks will work just fine. They are simple, easy to learn and flexible. They will allow you to build the largest, most complicated application you can think of.

There are some issues with unit testing, but using tools such as redux-mock-store and nock, you can handle most cases. You can see examples on the official recipes page.

Only look further if you feel you’ve mastered Redux and feel it’s not enough. Whatever you do, do NOT try to make your own middleware to handle asynchronous actions and side effects until you’ve already looked at what’s out there.

The search for the one true model

If thunks are so great, why are there so many smart people trying to come up with alternatives?

It generally falls in two big categories:

  1. They are harder to test
  2. Mixing actions and side effects in asynchronous action creators drastically increases complexity.

Other ecosystems, such as Elm (which heavily inspired Redux) use common functional patterns to tackle these problems. In the React/Redux/JavaScript world however, these are still open questions.

That’s why every couple of days, someone tries to find a more elegant way to solve these problems, and we end up with countless side effect “frameworks”. This is very much like how, until Redux became popular, a new Flux framework came out every other week. I don’t think it’s a bad thing. A better de facto model to handle the hardest piece of Redux development would be great.

I, however, think these tools should not be used as clutch to bypass problems with the JavaScript testing ecosystem. There are also a lot of common elements that each new attempt share, yet they are frequently presented as brand new ideas.

Testing is a solved problem

First, I’d like to dispel the notion that we need an alternative to thunks because of testing.

Yes, testing with redux-saga is beautiful, thanks to its declarative side effects, mocking dependencies work great too, and is often simpler. Look again at the Redux testing recipes under “Async action creator”.

Declarative side effects are a way to describe an effect, such a fetch call, as an object instead of calling the function directly. In Redux’s case, a middleware would then handle calling fetch for you. When unit testing, instead of mocking fetch, one can simply inspect the object to make sure it describes the call directly. The actual fetch function is never called.

It really isn’t that complicated, and many would argue it’s simpler than learning a whole new tool during development. You can keep using the simple thunk the way you always did.

The problem with this approach is that while well known global functions like fetch are easy to mock, the JavaScript ecosystem is anemic when it comes to mocking arbitrary things. In Java, C# or other classic OOP languages, this problem is solved via dependency injection frameworks that make it easy to substitute dependencies.

In JavaScript, dependency management tools such as Webpack or RequireJS complement module syntax such as ES6 modules, CommonJS or AMD to decide which modules to load. Allowing mocking and substitution of dependencies isn’t their primary goals. Frameworks such as Angular and Aurelia implemented their own dependency injection system in addition to JavaScript modules for that reason.

Many will argue that modules aren’t a substitute for dependency injection and breaks the inversion of control pattern. I disagree. The syntax makes it look that way, but the semantic is very much “I declare that I need this dependency”, and it is up to the module system to provide it. The fact they look like factory method calls is just syntax sugar. Adding another DI framework such as Angular’s is redundant.

We are left with weaker or more complicated tooling that is often environment specific.

Good, maintainable, decoupled code is easy to test and limits the need for mocks. That is one of the value propositions of the alternative side effect models. However, I think before we do that, the quality of the testing tools available in JavaScript should be improved. There will always be scenarios where you need to mock a dependency and warping high quality code for no other reason then testing is counterproductive.

I really think the community should not let efforts to make writing testable code easier stop it from developing better testing tools. They’re complementary. We need both.

Don’t ignore what has been done before

This is a bit of a pet peeve of mine. Every couple of days, someone who has been learning React/Redux for a few weeks comes up with this revolutionary idea to reduce boilerplate or. in this case, to handle side effects. The vast majority are just rehash of the 60 other attempts that were made before.

Worse, they usually haven’t even tried the existing solutions, which often would have solved their problem already. Recently I’ve seen one where the author hadn’t fully grasped redux-thunk before starting to make their own. Thunk is 8 lines of code including the blank line and brackets. Sagas, Loop and the others may not be perfect, but they are very solid attempts at simplifying side effects.

More importantly, across all the attempts we’re starting to see clear themes coming up, and any new tool should explain how they’re working with those themes, or why they did not. Here’s some of the themes I look for in every new tool I look at.

Declarative side effects

To avoid callback hell, simplify the control flow, and make testing easier (oh no, I fell for it too!), there’s a lot of gain to be made in having declarative side effects be a first class citizen. Almost all of the attempts at side effects models go that route.

Synchronous actions only

Sagas, Loop and actors all get rid of asynchronous actions altogether. Action creators go back to the default (returning an action object), and some kind of external listener or even the reducer takes care of side effects. If the side effect isn’t declarative, that just moves the problem elsewhere, though.

Changing the Redux pattern

Redux is a design pattern, and it’s great, but many people like to experiment with tweaking the pattern. In Redux-loop for example, modifying state is considered just one of many side effects handled by the reducer, which is a significant change. Those aren’t necessarily bad things, but authors should be clear about what they’re changing and what are the trade offs.

Comparison with Elm or functional programming

Redux takes a lot from Elm and functional programming in general, and for good reasons. FP patterns are great at keeping function simple, pure, testable and keeping state or side effects isolated.

When bringing forward a new way to handle promises, http requests or other side effects, it’s likely there are parallels in Elm, Haskell or Elixir communities, to name a few. I like to see these brought up in the conversation. “We’re doing things this way because it is a problem that was solved in FP language XYZ and it worked well”. Let’s not reinvent the wheel.

The next leap

Like Redux did for Flux, I think we’re getting close to the point where one of those middlewares or tool will raise above the rest as THE way to go when developing React applications. It’s possible one of the existing solution will be it, or that a new one will come out of nowhere.

To get there though, we have to attack the right problems, and build on top of what we already have instead of starting over every time as if no one else tried before.