Note: This article assumes that you have some basic knowledge of how Redux works.
If there is a piece of functionality that we want to interject between the triggering of any action from a component and the service of this actions from a reducer ,we can use middlewares. Middlewares provide us a way to hook between these two points of execution and add some code we want to execute for actions. Of course, you may think that this pieces of functionality could be interjected into the top-level reducer. BUT:
(a) it is not that convenient. Especially if part of this functionality applies before the reducer logic is engaged and part after the new state has been formed.
(b) De-coupling! We want to be able to write this “middleware” functionality at the same time when someone else is working on the reducer.
(c) SRP. We don’t want the top-level reducer to grow very big by integrating any kind of functionality that applies to all actions.
Here is a snippet of how it works:
import { applyMiddleware } from 'redux'; // Define a middleware const LoggerMiddleware = (store) => (next) => (action) => { console.log("Logged action type: ", action.type); next(action); }; // Add the middleware to the store. const store = createStore(reducer, initialState, applyMiddleware(LoggerMiddleware));
Is this magic for you ?
What we really want to achieve is put this functionality in the dispatch() method of the Redux store. So, every time we want to dispatch an action, this functionality will be engaged.
Redux documentation explains ( https://redux.js.org/advanced/middleware ) in a very nice way how this can be achieved through the decorator pattern. In a nutshell, the “trick” works like this:
// This middleware function accepts the store and returns // a new dispatch method for the store. This new method includes // the middleware functionality function loggerMiddleware(store) { let oldDispatchFunction = store.dispatch return function newDispatchFunction(action) { console.log('dispatching', action) let result = oldDispatchFunction(action) console.log('next state', store.getState()) return result } } function anotherMiddleware(store) { } // For each middleware function, replace the dispatch() // method of the store store.dispatch = loggerMiddleware(store); store.dispatch = anotherMiddleware(store); ...
As you may understand, each middleware is using/calling the dispatch function that has been created by the previous middleware. So, the order matters. Just think that a middleware could also modify the action object.
The whole process can be hidden by a wrapper function like this:
function applyMiddlewareFunctions(store, middlewares) { middlewares = middlewares.slice() middlewares.reverse() // Transform dispatch function with each middleware. middlewares.forEach(middleware => store.dispatch = middleware(store) ) }
Of course, this is not all, but it is the main and biggest part of the idea.