In a previous article I wrote about how Redux stores provide a dispatch function to “send” action objects to the reducer in order to update the store’s state.

But what if we want to do something before or after each and every dispatch such as logging, centralised error handling or allowing dispatch to handle action creators which return Promises?

The solution to this is called Redux Middleware.

What is Middleware?

According to the official Redux documentation:

Middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

https://redux.js.org/advanced/middleware

To do this middleware effectively wraps the store.dispatch function with custom implementations which provide additional functionality.

You can think of this as (roughly) akin to reassigning the store.dispatch to another function which does something new and then calls the original dispatch function.

In the example below, the original store.dispatch is overwritten and replaced (monkey-patched) with an enhanced dispatch function logAllActionTypes which wraps the original dispatch function to provide additional logging functionality.

/**
 * DO NOT COPY - example code only
 */
const logAllActionTypes = (store) => {
    // Reference to original dispatch
    const originalDispatch = store.dispatch;
    
    // Return "new" dispatch function with same signature.
    return (action) => {
        // Add additional functionality
        console.log(action.type);

        // Call original dispatch with the action.
        const returnValue = originalDispatch(action);

        return returnValue;
    } 
}

const store = createStore();

// Enhance original store
store.dispatch = logAllActionTypes(store);

A better solution with applyMiddleware

Monkey patching store.dispatch as we do above is not a great approach. As a result, Redux provides a utility called applyMiddleware that solves the problem of enhancing dispatch in a more elegant fashion.

It does this by allowing you to define a series/chain of middleware functions. Each function is curried in order to provide access to store and the “next” wrapped dispatch function in the middleware chain.

Here’s our example above rewritten as a middleware function compatible with applyMiddleware

const logAllActionTypes = store => next => action => {
    // Add additional functionality
    console.log(action.type);

    // Call next dispatch with the action.
    const returnValue = next(action);

    return returnValue;
}

createStore( someReducer, [], applyMiddleware( logAllActionTypes ) );

Note that it is the “inner” function (the one that accepts action as a parameter) which is called when an action is dispatched. However, this function has access to the store (for convenience) and is able to call the next wrapped dispatch function in the chain in order to ensure that middlewares are fully composable.

Understanding Redux’s applyMiddleware

To understand how this works consider the following lines from Redux’s applyMiddleware implementation.

First a subset of the store’s API is referenced. This reduced API is what will become the (first) store argument of all the curried middleware functions:

const middlewareAPI = {
    getState: store.getState,
    dispatch: (action, ...args) => dispatch(action, ...args)
}

Next a “chain” of middleware is created by applying middlewareAPI to all the middleware functions:

const chain = middlewares.map(middleware => middleware(middlewareAPI));

Finally, a new dispatch is created by composing the result of calling all the middleware functions from right to left. The “rightmost” function receives the original store.dispatch, with each function applying new functionality until a new enhanced dispatch is returned.

dispatch = compose( ...chain )( store.dispatch );

Each middleware requires no knowledge of what comes before or after it in the chain with the exception that it must call the next function.

It is this composability that is the key feature of middleware and is what makes it incredibly flexible.

Further learning

For more information on understanding and using Middleware within Redux please see:

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.