Redux Middleware Explained
How Does Redux Middleware Actually Work?
Whether you're looking to add some customization to your Redux workflow or just looking to understand how Redux works a little better, it's great to know exactly what Redux is doing under the hood with middleware.
I've been going down this rabbit hole trying to write my own Redux middleware so I thought I'd share my findings.
Fortunately for us, Redux turns out to be fairly simple, so let's take a closer look at the Redux source code and try to figure out what's going on.
I'll pull examples from the Redux source code and type definitions, but I'll be stripping it down for simplicity, mostly removing some error-handling and subscription management code (subscriptions aren't particularly relevant for us here). Let's start with the primitives and work our way up.
Examples are based on Redux 4.0.4
What is an Action
?
Action
?Most strictly, an Action
is an object with a field type
. More usefully, an Action
describes what change should be made to the application state.
What is a Reducer
?
Reducer
?A Reducer
is a function that, given an existing state and an Action
, produces the state that should exist after the Action
has been applied. If an Action
describes what changes should be made, a Reducer
describes how that change should be made.
What is a Store
?
Store
?Very simply, it's just an object that provides us a way to get the current application state (getState()
) and provides a dispatch
function that allows us to send Action
s to our Reducer
s. A simplified interface would be something like this:
We create our Redux store using the createStore
function, which, well, creates our Store
. More importantly, it contains some important variables that will get used by functions like dispatch
.
What's important is that dispatch
and other functions are also created in createStore
, meaning that these variables are closed over, i.e. dispatch
will have access to these variables, and we'll see how it uses them next.
What does dispatch
actually do?
dispatch
actually do?I'll be honest, I was a little surprised at just how simple the dispatch
function is when I first saw it (though I don't really know what I expected). You'll see that is uses those variables defined in createStore
from before. Here's a stripped down version, showing just the meat of it:
Well, that's about it. It reassigns currentState
to the new application state the reducer spit out in response to an Action
.
Keep in mind, there is only one Reducer
– don't let the variable name currentReducer
throw you off. It's only "current", because of some functionality Redux provides for dynamically loading reducers that isn't relevant for us here. Speaking of which, how does Redux make all our different reducers act as one?
How does Redux combine reducers?
Redux provides the combineReducers
function, which takes a object-map of sub-reducers and returns a single function, i.e. a Reducer
, that invokes all of them. Here's a simplified version of combineReducers
.
Again, just like dispatch
, this is actually pretty simple. We go through all the sub-reducers we have, pass it existing state and the action, and collect all those results into a new state object.
What does a Redux Middleware
look like?
Middleware
look like?Middleware is where this gets a little more complicated (which is why it was important to go over the building blocks!).
Middleware allows us to add all sorts of functionality to Redux between the points when an action is first dispatched to when it's sent to the reducer. It also allows us to enhance the dispatch
function to add to its capabilities.
Let's examine all the relevant code.
MiddlewareAPI
MiddlewareAPI
A MiddlewareAPI
is what gives our middleware access to dispatch
and a way to get the current state. Simple enough.
Middleware
Middleware
This type definition might be a little hard to follow, but notice that the last function in the signature just takes in an action, i.e. the same as dispatch
.
I think the best way to think about it is that every middleware is actually just a dispatch
that has a handle to the next dispatch in the chain. So really, our middlewares are just custom dispatch
s that handle the action in some way and then pass the action off to the next dispatch. You may need to stew on that for a minute and make sure you understand it.
The reason this signature needs to be curried is that the custom dispatch
we create needs access to the MiddlewareAPI
and a handle to the next middleware/dispatch in our middleware chain. All that happens in applyMiddleware
.
applyMiddleware
applyMiddleware
Basic type definition.
I'm assuming you have a baseline understanding of curried functions in JavaScript here I think a helpful way to conceptualize deeply curried functions is as just a single function with multiple 'layers' of parameters. The 'layers' get 'peeled off' when you apply a argument to it, which allows us to share common data between multiple curried functions. In this way, a Middleware
is a curried function with three layers – an APIMiddleware
, a Dispatch
, and finally an Action
. applyMiddleware
'peels off' two layers of our Middleware
s, creating a single function that becomes our Redux Store's dispatch
.
To 'peel off' the first layer, we first create a MiddlewareAPI
from our newly created store and give all our middlewares access to it with middlewares.map(middleware => middleware(middlewareAPI))
.
To 'peel off' the second layer, we chain our Middleware
s together with the compose
function. compose
serially chains multiple functions together into a single function. It's actually not terribly complicated, but it's easier to understand by example: compose(F, G, H)
turns into (...args) => F(G(H(...args)))
. (Note that in this chain, the parameters that H
accepts become the parameters that our newly created chain accepts.) Finally, we call this composed Middleware
chain with the default dispatch
from our store.
With the second layer peeled off, now our middlewares
are just normal dispatch
s (Action => any
) that have a handle to next
i.e. the next dispatch
in the chain. (That means we have to make sure our Middleware
calls next
at some point, or the dispatch won't make it to the end i.e. our Reducer
!)
I'll try my best to illustrate that process the best I can.
Calling our composed chain with store.dispatch
will make store.dispatch
the final dispatch
in the chain. Here's some pseudocode that outlines how that application would go.
And now here's what our final dispatch
will look like. This is, again, pseudocode to illustrate how the data flows, not the definitions of the functions.
The final bit of applyMiddleware
is returning our store
, but with dispatch
overridden to be our custom dispatch
.
And finally, we've created our Redux store with our middleware applied!
Try It Out
I hope this look into Redux has given you what you need to start tinkering yourself. I did most of this research while figuring out how to create a custom middleware of my own. It's certainly within reach to try it yourself – maybe a logging framework, maybe a network call abstraction layer, maybe something even wilder. Go ahead and tinker with it and see what you can come up with.
Last updated