Dissecting the store enhancer of Redux<!-- --> | <!-- -->Patrick Desjardins Blog
Patrick Desjardins Blog
Patrick Desjardins picture from a conference

Dissecting the store enhancer of Redux

Posted on: February 6, 2018

In previous articles, we dissected the compose function of Redux and the createStore function. The createStore uses the compose function to have many store enhancer. In this article, we will see an example of a store enhancer by dissecting one of the most used store enhancer: applyMiddleware.

It might be a surprise to some that Redux doesn't have baken in Redux the notion of middleware. While it is part of the package, it uses the core notion of store enhancer to bring the middleware concept in life. To have middlewares being executed between a dispatch call on the store and reducers to alter the store's state, an alteration of how dispatch work must be done. As we learned, store enhancer allows to modify the store object that contains the dispatch and this is what applyMiddleware does.

Before discussing more, let's see the Redux applyMiddleware function.

1export default function applyMiddleware(...middlewares) {
2 return (createStore) => (...args) => {
3 const store = createStore(...args);
4 let dispatch = store.dispatch;
5 let chain = [];
7 const middlewareAPI = {
8 getState: store.getState, dispatch: (...args) => dispatch(...args)
9 };
10 chain = middlewares.map(middleware => middleware(middlewareAPI));
11 dispatch = compose(...chain)(store.dispatch);
13 return { ...store, dispatch }
14 }

The function takes a list of middlewares. This is custom to applyMiddleware which let have a list of composed function that will be called. If you are building your own store enhancer, you will need to pass whatever you anticipate being needed to modify the store behavior. What is important is the second line which returns a function that has the createStore function that returns a function with many arguments which return the store. In that case, the last return of the function returns a copy of the store (spread operator) with the dispatch function redefined.

Let's rewind a little bit. The return function that returns a function is actually calling createStore with the args list of argument. At that point, the function could return the result and nothing would be changed. Obviously, something will be done. First, the method takes a reference to the actual dispatch function. Then, it creates an object that will be passed down to all middleware. This object has the actual store's state and a dispatch function that call the original dispatch function. The whole middleware logic is the next line which will invoke all the middleware passed by parameter one at a time. The middleware passed by parameter are functions are well, which take the "middlewareAPI" object. At that point, we only have middleware function that is having access to the getState and dispatch functions. The next line will chain them by passing the store.dispatch function. Every middleware has access to the next one by calling "next". Often you will see that people invoke "next" with a new action. When doing so, they call the next middleware with a new action which is totally valid but won't go through the whole chain, just forward. The store.dispatch passed is there to trigger at the end of the chain the reducer.

The return of each middleware is the result of the previous middleware. The return will be used by the following middleware a way to traverse the chain of middleware by calling next on the action.

Here is a middleware example that console.log before and after a dispatch is invoked. The format is disquieting at first with the equal sign followed by the three arrow functions. You do not have to use this sugar syntax, but it reduces quite a lot and you will often see this written format. What it does is to store the middleware in the "logger" variable. The variable is a function that has for parameter the store. The store contains the "middlewareAPI", hence getState and dispatch. As we saw, the composition calls the chain of middleware with "store.dispatch" and return the next middleware which is referred by the named "next". Finally, the action being dispatched is available.

1const logger = store => next => action => {
2 console.log("Before", action);
3 const result = next(action);
4 console.log("After", store.getState());
5 return result

For completion, let's analyze a very popular middleware called Thunk which brings the possibility to have several dispatches. Because, so far, every middleware receives a getState and dispatch from "middlewareApi" and return the next action has an object which doesn't give the time to perform any asynchronous logic. The reason is that the function applyMiddleware is composing all middleware, hence you have a function with "next" that lets you chain them. Finally, the function with the parameter "action" which is the what is being dispatched.

The Thunk middleware looks up to see if the action dispatched is a function or an object. Normally, it would be an object with the required "type" member defined (see the createStore that throw an exception otherwise). The Thunk middleware invokes the action by passing the "dispatch", "getState" and "extraArgument" and it returns the result of the action. The "dispatch" function passed down lets you invoke several time actions if needed while the getState function lets you peek at the current state to perform some business logic in your middleware. For example, you could see if some data is missing in the store to invoke some APIs.

1function createThunkMiddleware(extraArgument) {
2 return ({ dispatch, getState }) => next => action => {
3 if (typeof action === 'function') {
4 return action(dispatch, getState, extraArgument);
5 }
6 return next(action);
7 };

With Thunk in the middleware composition and dispatching a function, what happens is that other middleware will ignore the function and the thunk will catch that it's an action and then invoke the action.

1export function asyncMethodThatFetchData(inputData) {
2 return function (dispatch, getState) {
3 dispatch(actionLoadingData());
4 return fetch(`https://www.api.com/${inputData.id}`) .then(json => dispatch(actionDataLoaded(inputData, json)); )
5 }

This simple function is dispatched by the store with the argument "123". It will be intercepted by the Thunk which will execute the return of the function which is the function that takes the dispatch and getState argument. Within the function, there is a first dispatch that is called which could be used to start a loading animation and a second one once the data has been fetched.