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.
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.