Exploring ApplyMiddleware: How it works behind the scenes

pandresdev

Andrés Valdivia Cuzcano

Posted on July 5, 2023

Exploring ApplyMiddleware: How it works behind the scenes

In this post we will talk about middleware in Redux and how to make them composable, we will also explain step by step how applyMiddleware works to wrap the store's dispatch and add functionality to it.

Middleware

A middleware is just a function that allows us to wrap the store's dispatch method to add logic before and after dispatching an action.

To do this, middleware must wrap the store's dispatch method, so middleware can be composed of other functions, either other middleware or the dispatch method. This is why a middleware has the following shape:



type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }

type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch


Enter fullscreen mode Exit fullscreen mode

We can see that the first function receive a “store” (MiddlewareAPI) as parameter, so we can pass one as an argument. Later I will explain in detail how it works.

applyMiddleware

applyMiddleware(...middleware) is a special type of enhancer, so it is also a higher-order function, this enhancer combines all the middlewares that are passed as parameters to create a single middleware that wraps the store's dispatch method. Let's see how it chains the middleware.

For this example we will use the following three middleware:



const firstMiddleware = (store) => (next) => (action) => {
  console.log("First middleware");
  const result = next(action);
  console.log("First middlware done");
  return result;
};

const secondMiddleware = (store) => (next) => (action) => {
  console.log("Second middleware");
  const result = next(action);
  console.log("Second middlware done");
  return result;
};

const thirdMiddleware = (store) => (next) => (action) => {
  console.log("Third middleware");
  const result = next(action);
  console.log("Third middlware done");
  return result;
};


Enter fullscreen mode Exit fullscreen mode

As we have seen, a middleware receives an object similar to a store and returns a function that receive the next middleware as a parameter, in order to make them composable, so we could do the following.:



import { createStore, compose } from "redux";

const store = createStore(rootReducer, initialState);

const firstChain = firstMiddleware(store)
const secondChain = secondMiddleware(store)
const thirdChain = thirdMiddleware(store)

// Compose the middlewares and add the store dispatch as last "next" parameter
const enhancedDispatch = firstChain(secondChain(thirdChain(store.dispatch)))

// Another way to do this is to use the compose function
const enhancedDispatch = compose(firstChain, secondChain, thirdChain)(newStore.dispatch)


Enter fullscreen mode Exit fullscreen mode

What has been achieved by doing this is to chain the middleware and have the last middleware dispatch the action, since the latter returns the following function:



(action) => {
  console.log("Third middleware");
  // next === store.dispatch
  const result = next(action);
  console.log("Third middlware done");
  return result;
}


Enter fullscreen mode Exit fullscreen mode

As the last parameter is the dispatch method, the return of the last middleware is the function we see above, this function is the value of the next parameter of the previous middleware and so on, this way we obtain an "enhanced dispatch" where the action goes through all the middleware.

The following diagram shows how the middleware composition works:

Middleware composition

Add middleware to a store enhancer

So far we have seen how to compose the middleware and create an enhanced dispatch, so the next step is to add it to the store so that the middleware is executed every time an action is dispatched.

To do this, we going to create an applyMiddleware function that returns a store enhancer (Post about enhancer):



import { createStore, compose } from "redux";

// Receive middleware as parameter and return a store enhancer
const applyMiddleware = (...middleware) => (createStore) => {
  return (reducer, initialState, enhancer) => {
    const newStore = createStore(reducer, initialState, enhancer);

    const middlewareChain = middleware.map((middleware) =>
      middleware(newStore)
    );
    const enhancedDispatch = compose(...middlewareChain)(newStore.dispatch);

    return {
      ...newStore,
      dispatch: enhancedDispatch
    };
  };
};

// Create a store using the applyMiddleware function
const store = createStore(userReducer, initialState,
  applyMiddleware(firstMiddleware, secondMiddleware, thirdMiddleware)
);


Enter fullscreen mode Exit fullscreen mode

The applyMiddleware function receives an array of middleware as parameter and returns an enhancer, which will be passed as the third parameter of the function createStore and then we pass the new store as parameter of each middleware to obtain the function that will allow us to compose each middleware ((next) => {...}) and as we saw before we pass the dispatch method as parameter of the last middleware. Finally, we return a new store whose dispatch method will be composed of all middleware.

From now on, every time an action is dispatched, the following flow will be executed.

applyMiddleware flow

logs when a action is dispatched

💖 💪 🙅 🚩
pandresdev
Andrés Valdivia Cuzcano

Posted on July 5, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related