You've seen middleware in action in the "Redux Fundamentals" tutorial. If you've used server-side libraries like Express and Koa, you were also probably already familiar with the concept of middleware. In these frameworks, middleware is some code you can put between the framework receiving a request, and the framework generating a response. For example, Express or Koa middleware may add CORS headers, logging, compression, and more. The best feature of middleware is that it's composable in a chain. You can use multiple independent third-party middleware in a single project.
Redux middleware solves different problems than Express or Koa middleware, but in a conceptually similar way. It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.
This article is divided into an in-depth intro to help you grok the concept, and a few practical examples to show the power of middleware at the very end. You may find it helpful to switch back and forth between them, as you flip between feeling bored and inspired.
While middleware can be used for a variety of things, including asynchronous API calls, it's really important that you understand where it comes from. We'll guide you through the thought process leading to middleware, by using logging and crash reporting as examples.
One of the benefits of Redux is that it makes state changes predictable and transparent. Every time an action is dispatched, the new state is computed and saved. The state cannot change by itself, it can only change as a consequence of a specific action.
Wouldn't it be nice if we logged every action that happens in the app, together with the state computed after it? When something goes wrong, we can look back at our log, and figure out which action corrupted the state.
How do we approach this with Redux?
The most naïve solution is just to log the action and the next state yourself every time you call
store.dispatch(action). It's not really a solution, but just a first step towards understanding the problem.
If you're using react-redux or similar bindings, you likely won't have direct access to the store instance in your components. For the next few paragraphs, just assume you pass the store down explicitly.
Say, you call this when creating a todo:
To log the action and state, you can change it to something like this:
This produces the desired effect, but you wouldn't want to do it every time.
You can extract logging into a function:
You can then use it everywhere instead of
We could end this here, but it's not very convenient to import a special function every time.
What if we just replace the
This is already closer to what we want! No matter where we dispatch an action, it is guaranteed to be logged. Monkeypatching never feels right, but we can live with this for now.
What if we want to apply more than one such transformation to
window.onerror event is not reliable because it doesn't provide stack information in some older browsers, which is crucial to understand why an error is happening.
Wouldn't it be useful if, any time an error is thrown as a result of dispatching an action, we would send it to a crash reporting service like Sentry with the stack trace, the action that caused the error, and the current state? This way it's much easier to reproduce the error in development.
However, it is important that we keep logging and crash reporting separate. Ideally we want them to be different modules, potentially in different packages. Otherwise we can't have an ecosystem of such utilities. (Hint: we're slowly getting to what middleware is!)
If logging and crash reporting are separate utilities, they might look like this:
If these functions are published as separate modules, we can later use them to patch our store:
Still, this isn't nice.
Monkeypatching is a hack. “Replace any method you like”, what kind of API is that? Let's figure out the essence of it instead. Previously, our functions replaced
store.dispatch. What if they returned the new
dispatch function instead?
We could provide a helper inside Redux that would apply the actual monkeypatching as an implementation detail:
We could use it to apply multiple middleware like this:
However, it is still monkeypatching. The fact that we hide it inside the library doesn't alter this fact.
Why do we even overwrite
dispatch? Of course, to be able to call it later, but there's also another reason: so that every middleware can access (and call) the previously wrapped
It is essential to chaining middleware!
applyMiddlewareByMonkeypatching doesn't assign
store.dispatch immediately after processing the first middleware,
store.dispatch will keep pointing to the original
dispatch function. Then the second middleware will also be bound to the original
But there's also a different way to enable chaining. The middleware could accept the
next() dispatch function as a parameter instead of reading it from the
This is exactly what Redux middleware looks like.
Now middleware takes the
next() dispatch function, and returns a dispatch function, which in turn serves as
next() to the middleware to the left, and so on. It's still useful to have access to some store methods like
store stays available as the top-level argument.
applyMiddlewareByMonkeypatching(), we could write
applyMiddleware() that first obtains the final, fully wrapped
dispatch() function, and returns a copy of the store using it:
The implementation of
applyMiddleware() that ships with Redux is similar, but different in three important aspects:
It does a bit of trickery to make sure that if you call
store.dispatch(action)from your middleware instead of
next(action), the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware. There is one caveat when calling
dispatchduring setup, described below.
To ensure that you may only apply middleware once, it operates on
createStore()rather than on
storeitself. Instead of
(store, middlewares) => store, its signature is
(...middlewares) => (createStore) => createStore.
Because it is cumbersome to apply functions to
createStore() before using it,
createStore() accepts an optional last argument to specify such functions.
applyMiddleware executes and sets up your middleware, the
store.dispatch function will point to the vanilla version provided by
createStore. Dispatching would result in no other middleware being applied. If you are expecting an interaction with another middleware during setup, you will probably be disappointed. Because of this unexpected behavior,
applyMiddleware will throw an error if you try to dispatch an action before the set up completes. Instead, you should either communicate directly with that other middleware via a common object (for an API-calling middleware, this may be your API client object) or waiting until after the middleware is constructed with a callback.
Given this middleware we just wrote:
Here's how to apply it to a Redux store:
That's it! Now any actions dispatched to the store instance will flow through
If your head boiled from reading the above section, imagine what it was like to write it. This section is meant to be a relaxation for you and me, and will help get your gears turning.
Each function below is a valid Redux middleware. They are not equally useful, but at least they are equally fun.