Redux Funk—Simple, Testable Async Effects

Written by maxheiber | Published 2016/09/30
Tech Story Tags: react | redux | javascript | asynchronous | functional-programming

TLDRvia the TL;DR App

There are a ton of libraries for doing asynchronous effects in Redux apps. Some see this as a bad thing, but I love the intellectual curiosity and willingness to explore in the JavaScript community.

There wasn’t a Redux effects library that met my needs, so I ended up making something new. I’ll share the ideas I borrowed from other effects libraries and how they are used in redux-funk.

For some background, there’s a Stack Overflow post that goes into why you’d want a tool for asynchronous effects in Redux apps.

Redux Loop—Declarative effects in reducers

The example below demonstrates some of the great ideas in Redux Loop. It’s code for a reducer that returns an instruction to increment a counter after a delay:

Redux Loop Example

The call to the loop function above can be read as “return the state unchanged and call incrementAsync with arguments [action.delay].

An advantage of putting async stuff into reducers is that there is now just one place to look when trying to figure out “What does the app do when this action is dispatched?”. Whether an action should be handled synchronously or asynchronously becomes an implementation detail, rather than a decision that effects what part of the app your code lives in. For comparison, with Redux Thunk, async stuff lives in action creators and with Redux Saga async stuff lives in sagas.

A big rule in Redux is that reducers should be pure. It may initially seem that Redux Loop violates this rule, but all is well. Calling the loop function does not actually perform any side effect. Instead, loop returns a data structure representing the next state and effect. This is what makes it really easy to write tests—you can use deep equality checks to see what effects get returned:

Test for a reducer that uses Redux Loop

I didn’t use Redux Loop, because I needed to do custom reducer composition, such as delegating to another reducer from within a reducer. This is complicated with Redux Loop, because its effects must be passed up the state tree using isLoop, getEffect, getModel, and batch.

The problem is that in reducer composition, we want the child reducers to only have access to part of the state tree, not the whole thing. So when storing effects on state (like in Redux Loop), the effect somehow has to make its way to the top of the tree so that the middleware can see it. The alternative is to do an entire tree traversal, looking for effects, which probably isn’t a good idea. I borrowed a solution from Redux Side Effect and describe it in the next section.

Here’s the redux-funk version of the async counter reducer:

Reducer using Redux Funk for declarative effects. I explain how call is implemented in the next section.

In the code above, the call is used to queue up an effect. The effect [incrementAsync, [action.delay]] can be read as “call incrementAsync with action.delay as its first argument.” I use funks to refer to effects of the form [func, [arguments…]]. redux-funk adds a funks array at the top of the state tree to queue up async effects. So to test a reducer that uses redux-funk, you can just check the funks key:

Redux Side Effect—Using the action object

Redux Loop code has to sometimes jump through hoops to pass effects up the state tree. Redux Side Effect avoids those difficulties and has a tiny line count because of a trick—it uses the action object to communicate effects.

The following code shows how you can add a side effect in a reducer using Redux Side Effect:

Redux Side Effect example

The Redux Side Effect middleware just checks the effects on the action and calls them all with dispatch as the first argument. Using action is a shortcut: while child reducers get only branches of the entire state tree, action is shared across all reducers.

In spite of the elegance of this solution, I ended up not using Redux Side Effect because:

  • The effects are non-declarative. In the example above, the only way to test that the delay is correct is to actually set a delay and compare timestamps.
  • Reducers become dependent on the presence of the sideEffect method.
  • Mutating the action object breaks the Redux paradigm, which I’d like to avoid if possible.

redux-funk also uses the trick of storing actions on the action object to avoid having to pass effects. But the implementation maintains the purity of reducers. call(action, funk) adds the funk to an action, using a symbol as a key:

Implementation of the call function in redux-funk

When using redux-funk, you call coalesceReducers on the top-level reducer, which takes the queue of effects from the action and puts them onto the store state. This restores the action to the way it was, and, more importantly, makes the funks available to store consumers. The testing example above used coalesceEffects.

Here’s the implementation of coalesceEffects:

Implementation of coalesceFunks

From React-Redux —Subscribing to the Store

React-Redux contains helpers for using the state to render the UI, which is arguably the most important side effect. Most effects libraries for Redux are middleware, but, instead, React-Redux subscribes to the store and reacts to the current state.

Since redux-funk makes effects as part of store state, you can just subscribe to the store and then program with the effects. I made a small helper, runFunks, for handling effects in a way similar to how Redux Loop does it:

Helper for working with declarative effects

runFunks enables the effects described in reducers to actually get called. In the counter example, the function returns a promise for an action. runFunks calls the function and holds onto the promise for the INCREMENT action. When the promise resolves, runFunks dispatches the action:

Reducer using Redux Funk for declarative effects

Here’s the implementation of runFunks:

Implementation of runFunks

You can use redux-funk without runFunks, and implement your own logic for handling. That’s one of the advantages of storing effects on state. For example, you could use a variant of the code above to add delays to every effect, debounce, use callbacks instead of promises, do remote procedure calls, inject dependencies into funks, etc.

Try it Out

If you’ve made it this far, you’ve seen the entire source code for redux-funk!

Hope you get a chance to try it out and play with the examples, which are adapted from the Redux Saga examples.


Published by HackerNoon on 2016/09/30