krawaller


Nine simple stand-alone experiments to understand Redux Middlewares

Exploring Redux middleware

Tags: redux

0. The premise

This blog post is made up by 9 tiny self-contained experiments aiming to better our understanding of Redux middlewares. You are assumed to be familiar with the basic functionality of Redux. If you're not then go remedy that immediately, and let it be known that I'm severely jealous of you for still having that experience in front of you!

Throughout we'll use a ridiculously simple counter reducer, based off of Redux creator Dan Abramov's minimal Redux example. It knows only of a single action, INCREMENT, and when that is encountered it increases state with the by payload (thus the state is not an object but a simple number):

var reducer = function(state,action){
switch(action.type){
case 'INCREMENT': return (state || 0) + (action.by || 1);
default: return state;
}
}

Just to try this out we can create a store with no middlewares:

var store = Redux.createStore(reducer);

Assuming a div with id app we whip up a render function:

var render = function(){
var newhtml = "<h2>Clicked "+store.getState()+" times.</h2>";
document.getElementById("app").innerHTML = newhtml;
}

We hook render up to run when the store updates, and we also run an initial render:

store.subscribe(render);
render();

Finally we set a click event which dispatches an action to the store:

document.addEventListener('click', function(e){
store.dispatch({ type: 'INCREMENT', by: 1 });
});

Try it out in the iframe below (or standalone here)

The following experiments will all be slight variations of this little app. Each will have a standalone link to an html document containing the full code, apart from Redux itself. There will be no other dependencies at all.

I'll also be avoiding ES6 syntax to make it more clear what's going on, as arrow functions and implicit returns tend to muddy the waters a bit.

1. The simplest possible middleware

A middleware sits on top store.dispatch. Each middleware is given a dispatched action object, doing their thing before passing it on to the next middleware in line, until it finally reaches the original store.dispatch which will call the reducer.

To get a sense for what the middleware looks like, check out this simplest possible middleware which does nothing but pass the action along:

var noop = function(middlewareAPI){
return function(next){
return function(action){
return next(action);
}
}
}

The middlewareAPI object we're being passed up top contains dispatch and getState, letting us do all kinds of things should we want to. Here we're only doing what we're supposed to, namely passing the action on to the next in line, which in this case will be the actual store.dispatch since there's no other middleware.

Just as a sanity check, let's create a store using this useless middleware...

var middlewares = Redux.applyMiddleware(noop);
var store = Redux.createStore(reducer,middlewares);

...and check that it is still functioning exactly like before (standalone here):

Things appear to still be working as they should.

2. A breadcrumb trail

In order to decipher what's going on, let's add a logging mechanism to our app! We add a <div id="log"/> to the page, and write to it using this output function:

// put stuff into the log
var output = function(txt){
var newparagraph = document.createElement("div");
newparagraph.innerHTML = txt;
document.getElementById("log").appendChild(newparagraph);
}

Now we create a logger middleware which uses output to spy on what's happening. Actually, let's make a loggerFactory(name), so we can have more than one logger with different names and track when the various parts of the middlewares are being called:

var loggerFactory = function(name){
return function(middlewareAPI){
output(name+": created");
return function(next){
output(name+": middle callback called");
return function(action){
output(name+": inner callback called with action "+JSON.stringify(action));
var ret = next(action);
output(name+": State after calling next: "+middlewareAPI.getState());
return ret;
}
}
}
}

Note how we use middlewareAPI.getState to query the updated state after the next call.

We create a store with two of these loggers:

var log1 = loggerFactory("log1"),
log2 = loggerFactory("log2"),
middlewares = Redux.applyMiddleware(log1,log2),
store = Redux.createStore(reducer,middlewares);

It's a bit spammy, but should give a good sense of what's going on (standalone here):

Matching the spam to the various parts of the middleware, here's what we can deduce:

function(middlewareAPI){ // called at start, left to right
return function(next){ // called at start, right to left
return function(action){ // called per action
// code before `next` call runs left to right
var ret = next(action);
// code after next call runs right to left
return ret;
}
}
}

3. Having some fun

Just to internalize our findings so far, let's make some silly middlewares that messes about with the next call! First we have a deaf middleware which will only hear what you say a third of the time:

var deaf = function(middlewareAPI){
var i = 0;
return function(next){
return function(action){
if (!(i++%3)) {
next(action);
}
}
}
}

Next we have nervous who will execute every action twice just to be sure:

var nervous = function(middlewareAPI){
return function(next){
return function(action){
next(action);
next(action);
}
}
}

Finally there's impatient who will set .by to 5 instead of the default 1:

var impatient = function(middlewareAPI){
return function(next){
return function(action){
// it is good form not to mutate action so we make a copy
next(Object.assign({},action,{by:5}));
}
}
}

We whip up a store using all three, in the mentioned order:

var middlewares = Redux.applyMiddleware(deaf,nervous,impatient),
store = Redux.createStore(reducer,middlewares);

The net result should be that only every third click registers, but that click will increase the counter by 2*5 (standalone here):

4. Snooping at final next

Since the action eventually ends up with the regular store.dispatch, that implies that next inside the final middleware is the vanilla store.dispatch. Let's see if that holds true! We make a middleware that makes the comparison:

var vanilladispatch = Redux.createStore(reducer).dispatch;
var snoopForVanilla = function(middlewareAPI){
return function(next){
return function(action){
var comparison = (next.toString() === vanilladispatch.toString());
output("next === vanilla dispatch? "+comparison);
return next(action);
}
}
}

Note how we cannot compare references directly since they obviously won't be the same (as we picked dispatch from a non-middlewared throwaway store), so we turn them to strings before we check equality.

Now, let's put it to the test (standalone here)!

It seems our assumption was correct!

5. Snooping at following middleware

This raises a question - what is next from the perspective of a middleware who isn't last in the chain? Let's revive or old friend the noop middleware:

var noop = function(middlewareAPI){
return function(next){
return function(action){
return next(action);
}
}
}

If we put it at the end and a snooper before it, what would that snooper actually see? An easy way to test that would be to simply log out next, so let's create a snooper doing that:

var snooper = function(middlewareAPI){
return function(next){
return function(action){
output("snooping at `next`: "+next);
return next(action);
}
}
}

We put both in a store, taking care to put snooper before noop:

var middlewares = Redux.applyMiddleware(snooper,noop),
store = Redux.createStore(reducer,middlewares);

...and voilĂ  (standalone here):

Evidently the inner part of the middleware becomes the next. Which makes sense as the inner part is what takes the action as an argument, and it is the action we're feeding to next!

Curious what would happen if we put snooper last in the chain? You'd get a facefull of Redux source code is what, spelling out the entrails of the dispatch method. Hack the standalone version and try it!

6. Dispatch return value

Ok, so the middleware passes the action along by calling the next it is given. But why does it return the result of that next call? Since the action chain is done through the next call, what purpose does that return value serve?

A clue might be that the documentation says that store.dispatch(action) returns the action you passed in. The docs also notes, however, that middlewares can mess with the return value.

First off, let's just hack the click handler to also output the return from dispatch to the log:

document.addEventListener('click', function(e){
var ret = store.dispatch({ type: 'INCREMENT', by: 1 });
output("return: "+JSON.stringify(ret));
});

We run this with a no-middleware store (standalone here):

Unsurprisingly it seems the docs told the truth - we simply get the action object back.

And, also unsurprisingly, if we insert a stupid middleware which returns some bogus crap:

var sillynoop = function(middlewareAPI){
return function(next){
return function(action){
next(action);
return "WOO!";
}
}
}

...then bogus crap is what comes out at the end of the chain, even if we put a regular noop before and after:

// setting up the store
var middlewares = Redux.applyMiddleware(noop,sillynoop,noop),
store = Redux.createStore(reducer,middlewares);

See for yourself (standalone here):

7. Localstorage getsate

We've utlized middlewareAPI.getState in the logger middleware. Here's another quick example of a more practical use; a persist middleware which uses middlewareAPI.getState to store the whole store state to localStorage for every change.

var persist = function(middlewareAPI){
return function(next){
return function function(action){
var ret = next(action),
str = JSON.stringify({data:middlewareAPI.getState()});
window.localStorage.setItem("SAVESTATE",str);
return ret;
}
}
}

We put this in a store, and seed it with data from localStorage as initial state:

var savedstr = window.localStorage.getItem("SAVESTATE")||"{}",
initialstate = JSON.parse(savedstr).data,
middlewares = Redux.applyMiddleware(persist),
store = Redux.createStore(reducer,initialstate,middlewares);

Give it a few clicks below, then reload the page to watch the counter remember its position (and not at all to give me extra page views).

Standalone here.

8. The injected dispatch

We've utlized middlewareAPI.getState in the logger middleware. The use of it is pretty obvious. But what about the other method on the middlewareAPI, namely dispatch? At first glance it doesn't really make sense. To continue the action chain we merely call next, as we've seen several times over. Why would we want to start a new chain?

Just to test that this is really what will happen (because who knows, maybe middlewareAPI.dispatch is a wormhole to the final dispatch in the chain?), let's make a silly echo middleware:

var echo = function(middlewareAPI){
return function(next){
return function(action){
if (action.type !== 'ECHO'){
setTimeout(function(){
middlewareAPI.dispatch({
type: 'ECHO',
volume: 3
});
},500);
return next(action);
} else if (action.volume){
setTimeout(function(){
middlewareAPI.dispatch({
type: 'ECHO',
volume: action.volume - 1
});
},500);
}
}
}
}

If the action it is given isn't an ECHO, it will start a new chain with an ECHO action with volume 3. If it was an echo it will repeat it with one less volume unit, until the echo fades. Note that we're swallowing all ECHO actions, not passing them along to next.

In order to see this in action let's also revive our loggerFactory, although slightly less spammy this time:

var loggerFactory = function(name){
return function(middlewareAPI){
return function(next){
return function(action){
var ret = next(action),
newstate = middlewareAPI.getState();
output(name+": called with "+JSON.stringify(action)+", state now "+newstate);
return ret;
}
}
}
}

We set up a store with the echo middleware sandwiched between two loggers:

var log1 = loggerFactory("log1"),
log2 = loggerFactory("log2"),
middlewares = Redux.applyMiddleware(log1,echo,log2),
store = Redux.createStore(reducer,middlewares);

Try it out (standalone here)!

9. Redux-thunk

So the previous experiment merely proved that middlewareAPI.dispatch is a way to start a new chain, something which on the surface seems pretty useless. Here's a very good example for when it is useful: Redux-thunk. It is a middleware where the source code looks like this:

var thunk = function(middlewareAPI){
return function(next){
return function(action){
if (typeof action === 'function'){
return action(middlewareAPI.dispatch,middlewareAPI.getState);
} else {
return next(action);
}
}
}
}

So what's going on here? It checks if action is a function (what?!), and if so it invokes that function with dispatch and getState from the middlewareAPI. If action is not a function, it simply passes it on to next as per usual.

The point of this is to enable developers to have action creators that are async. Let's take a look again at how the counter click handler is set in our experiments:

document.addEventListener('click', function(e){
store.dispatch({ type: 'INCREMENT', by: 1 });
});

In normal Redux usage, like for example in a React app using the React-Redux bridge, you won't pass around a reference to the store itself. Instead you'll pass around action creators, and have some central system feed whatever they return to store.dispatch. Translating our code above, that might look something like this:

// these action creators are what "views" in your app will use
var actionCreators = {
increase: function(inc){
// we return an action that'll go to the `dispatch` chain
return {type: 'INCREMENT', by: inc};
}
}

// The views will use the actionCreators through a bridge to the dispatch method:
var bridge = function(dispatch){
return {
increase: function(inc){ dispatch(actions.increase(inc)); }
}
};

The point of the bridge is that we don't pass the store or dispatch around, we just provide it at hookup time by passing it to the bridge function.

This model as seen so far doesn't really allow for defining async actions in a smooth way. Sure, we could do some async stuff in our bridge function above, but ideally we'd like to have that logic in the actionCreators definition. This is what Redux-thunk allows us to do. Remember how it checked if an action was a function, and if so invoked that? That allows us to do stuff like this (this is an extract from our Redux Firebase example):

var actionCreators = {
startQuoteEdit: function(qid){
return {type:C.START_QUOTE_EDIT,qid};
},
cancelQuoteEdit: function(qid){
return {type:C.FINISH_QUOTE_EDIT,qid};
},
deleteQuote: function(qid){
return function(dispatch,getState){
dispatch({type:C.SUBMIT_QUOTE_EDIT,qid});
quotesRef.child(qid).remove(function(error){
dispatch({type:C.FINISH_QUOTE_EDIT,qid});
if (error){
dispatch({type:C.DISPLAY_ERROR,error:"Deletion failed! "+error});
} else {
dispatch({type:C.DISPLAY_MESSAGE,message:"Quote successfully deleted!"});
}
});
};
},
// ...
}

As you can see, startQuoteEdit and cancelQuoteEdit are normal sync actions using regular plain action objects, while deleteQuote contains lots of logic doing both sync and async stuff.

When a deleteQuote action (which is a function) enters the dispatch chain, the thunk middleware will catch it, run it, and the result with be further calls to dispatch with (most often) regular action objects.

As you can imagine it is important to put thunk at the beginning of the middleware chain, since no other middleware will be expecting actions which are functions!

To further wrap our brains around this, let's make a contrived experiment of our own using thunk. Let's pretend-play that we're a real app and define our action like this:

var actionCreators = {
increase: function(inc){
return function(dispatch,getState){
dispatch({type: 'INCREMENTINCOMING'});
setTimeout(function(){
dispatch({type: 'INCREMENT', by:inc});
},1000);
}
}
}

We'll change the click hookup to look like this (although a "real" app would use a bridge of some kind and not a direct store reference):

document.addEventListener('click', function(e){
store.dispatch( actionCreators.increase(1) );
});

To see this in action we also make use of a simple logger:

var logger = function(middlewareAPI){
return function(next){
return function(action){
var ret = next(action),
newstate = middlewareAPI.getState();
output("action "+JSON.stringify(action)+", state now "+newstate);
return ret;
}
}
}

We take care to put that after thunk in the middleware chain:

var middlewares = Redux.applyMiddleware(thunk,logger),
store = Redux.createStore(reducer,middlewares);

Check it out (standalone here)!

This use was rather contrived, but again, remember - the point of thunks as actions is merely to allow us to write asynchronous action creators without being dependent on a reference to the store.

Wrapping up

I hope this little series of laboratory concoction was useful. Mind you, a good way to understand Redux is to read the source - it is tiny! In fact the full version with warnings and comments just has an ever so slightly larger character count than this blog post...