How Redux Works - Part 2

In this post we'll cover the various other files in the library. While some of these contain simple utilities, others are much more complex. We'll also cover the infamous redux-thunk.

How Redux Works - Part 2

This is part two of a two part series. For part one see here.

In our last post we covered the bulk of how Redux works with createStore. In this post we'll cover the various other files in the library. While some of these contain simple utilities, others are much more complex. We'll also cover the infamous redux-thunk. Before we get to redux-thunk though we need to cover applyMiddleware.

A note on embedded source: The most up to date redux source is available here. I'll be embedding source code as of when this post was written.

applyMiddleware

Now you might remember from part one that we have the notion of a store enhancer. These wrap the store and can modify it's exported functions. In this case, applyMiddleware is a store enhancer that wraps dispatch.

So when we create our store, we pass applyMiddleware with a middleware as the second argument.

This second argument triggers the following block from createStore:

Once again we're gonna be looking at functions all the way down. Enhancer is a function that takes createStore and returns a new version of createStore.

Upstream we've passed applyMiddleware(thunk) as the enhancer.

Let's take a look at just the function signature of applyMiddleware.

The first layer of applyMiddleware takes a list of functions, or middleware, that we're going to apply. Here's how we might call applyMiddleware in our application.

In this case we're only passing thunk but in another application we may want to pass more. applyMiddleware then returns a new function that takes createStore as it's argument.

This then returns a third function which takes the arguments that we would pass to createStore. In the code sample above, that would be reducer and preloadedState.

As an aside, the process of returning functions that each require a single argument is called currying. You can read more about that in Functional Programming Fundamentals.

Okay this third function is now the bulk of applyMiddleware. It has a bound copy of middlewares and createStore and takes whatever arguments we're going to pass to createStore.

Let's examine the body of this function, or at least the first half of it.

The first thing it does is, surprise, calls createStore with these arguments. Then it temporarily overwrites dispatch to make sure our consumers don't accidentally call it while we're setting up the store. This pattern should look familiar from part one, we're validating before doing business logic.

Okay now let's look at the entire function.

This function then loops over the list of middleware, which are functions, and calls them with the middlewareAPI. This gives those functions a bound reference to dispatch and getState. It then composes the list of middleware together (more on that later) and gives this new function access to the original dispatch that can only take a plain object.

Finally, it returns a new object with all the old methods on the store plus this new, wrapped dispatch.

Redux Thunk

While we're composing middleware, this is a great time to take a look at my personal favorite, Redux Thunk.

Wow, thats a dense 14 lines of code, and once again it's functions all the way down. Now might be a good time to refresh on currying. Let's step through this line by line.

The first bit is createThunkMiddleware, this function allows us to bind an extra argument to all thunks. This can be used as a way to inject dependencies into all of your action creators at runtime. For example, you might want to only allow API calls from within an action creator. This would let you enforce that.

You'll notice that by default this function exports a thunk middleware with no extra argument. Most people just use thunk without an extra argument.

Lines 2-8 are the bulk of the logic here. We return a function that takes {dispatch, getState. This is the same middlewareAPI you saw above. So now we have a bound function with dispatch, getState, and maybe an extraArgument.

Two more functions to go. This returns a function that takes the next middleware to call. This is effectively calling a chain of middlewares until we get to the plain dispatch. Finally, this returns a function that takes an action.

Lines 3-7 are now the actual thunk logic. If the action is a function, we call it and pass it dispatch, getState, and extraArgument. If it's a plain action, we just pass  that on to the next middleware or the original dispatch.

As an aside, this process is called trampolining in computer science.

Compose

Okay we've now seen applyMiddleware and redux-thunk. Let's zero back in on a section of applyMiddleware that should make a little more sense now.

This section calls each middleware with the middlewareAPI and then composes that chain together, with a final call to the original store.dispatch. Let's take a look at compose.

What's this doing? Well compose takes a list of single argument functions and passes them to each other from right to left. So compose(f, g, h)would be the same as (...args) => f(g(h(...args))).

You'll remember each middleware only takes a single argument at each phase, in this case that's next.

The first 8 lines are just optimizations. If we don't have any functions return an identity function and if we only have one function return that.

This last bit is the intimidating part:

return funcs.reduce((a, b) => (...args) => a(b(...args)))

What's going on here? Well, it's a reduce, another FP paradigm. It loops over funcs and then calls the function given to it with two arguments accumulator and value. In this case a is accumulator and b is value. accumulator is the return value of the previous function call.

Let's say we call compose(f, g, h). First it loops over the array [f, g, h]. Then this logic happens accumulator = (..args) = > f(..args). Then we get to g and call accumulator = accumulator(g) or accumulator = (...args) => f(g(..args)). Finally it gets to h and calls accumulator(h) which is equivalent to accumulator = (...args) => f(g(h(..args))).

If this is difficult to reason follow, don't stress. It took me a few hours to wrap my head around it.

bindActionCreators

While we're on the subject of actions. Let's talk about how our actions actually get access to dispatch. If you're using react-redux you probably just pass your action creators to mapDispatchToProps and let it take care of things for you. Well under the hood it's calling a function called bindActionCreators. Let's take a look:

Holy validation batman. Guess what, thats virtually all this function is doing. bindActionCreators either takes a map of action creators or a function that returns one. If it gets a function it just calls the singular bindActionCreator which we'll get to.

Otherwise it does a whole bunch of validation to make sure that our map is in the correct format.

Assuming that goes well, lines 15-23 loop over the map and create a new map with each function passed to bindActionCreator. Let's dig into that inner function now.

Were you expecting something other than a closure? bindActionCreator returns an anonymous function that calls the original function actionCreator with whatever arguments the anonymous function got and then passes the result of actionCreator to dispatch. Crazy, huh?

combineReducers

There's one file left in src and that's combineReducers.js. It's a relatively big one, 178 lines, so we'll break it down. Much like the rest of redux, there's a lot of validation however it only exports one function combineReducers.

This function takes a map of keys to reducer and returns a new rootReducer that slices up the redux state amongst these keys. The callsite looks like this

In this case the wizardReducer will receive the slice of the store from the wizard key down and the muggleReducer will receive the slice of the store from the muggle key down.

Back to the actual redux source, let's take a look at the declaration for combineReducers.

These 29 lines are once again validation on our arguments. Redux loops over all the keys, makes sure they're all defined and point to a function. Interestingly, if it's not a function it doesn't bother complaining.

It also sets up the unexpectedKeyCache which is a reference to any unexpected keys we've warned about.

It then calls assertReducerShape our first big validation function.

This is a pretty nifty function, it loops over each key and makes sure they handle an INIT action and a randomly generated PROBE_UNKNOWN_ACTION properly. This ensures that all reduces return a defaultState when given no initialState and return something when they get an unknown action.

Back to combineReducers, after the validation phase it returns our new rootReducer.

But obviously, our rootReducer must do some validation of it's own! First off, if assertReducerShape threw an error, it throws that error on every single invocation. It doesn't allow us to fail silently here.

Second, if we're in dev mode it calls getUnexpectedStateShapeWarningMessage.

This is a big function but the bulk of the logic is right here.

Basically, loop over the reducer keys and make sure they're identical to the state keys. Also remember any keys we've warned about in the past so we don't warn about the same issue repeatedly.

Finally, we get to the real logic below.

This loop takes every action and passes it to each reducer with the state it previously generated. It then does a simple reference check on each reducer's new state. If any of them are different, it returns a new object. Otherwise it just returns the previous state so upstream functions can memoize.

And that's redux! I hope you enjoyed this.