Redux Middlewares: A Missing Guide for Newcomers
Every real-world React Redux app makes extensive use of async requests. Redux doesn’t support them out of the box. So if you work in a frontend development agency and want to communicate with your backend colleagues (awesome guys!) and move your project to the next level, you will need to use middlewares.
After reading, you will know:
- How redux-thunk works?
- What is a middleware?
- How to create logging middleware?
- How do we apply middlewares to Redux store?
This is a pretty advanced topic, you should have a good grasp of React, Redux and functional programming to fully grasp the following material.
What is a middleware?
Middlewares are all about creating functions that will be composed together before the main dispatch method will be invoked.
With middlewares, you can create a point in time between dispatching an action in component and its getting into your reducer. You can take an advantage of this opportunity and perform all sort of things: make an api call, log actions payload or encapsulate any kind of logic that will enhance your workflow.
How to create a logging middleware?
The easiest way to understand middlewares is to see how existing ones work and write your own afterwards.
Lets analyze simple middleware that will log store state before and after action got dispatched:
export default function createLoggerMiddleware({ getState, dispatch }) {
return next => action => {
const console = window.console;
const prevState = getState();
const returnVal = next(action);
const nextState = getState();
console.log(`Previous state: ${prevState}`);
console.log(`Dispatching action: ${action}`);
console.log(`Next state: ${nextState}`);
return returnVal;
};
}
Our createLoggerMiddleware
factory function accepts getState and dispatch – those parameters are injected by the applyMiddleware
function that is used to attach middlewares to Redux store.
Next param is a function that lets us pass a value, usually received action, down the middleware chain and right into the reducer.
To take advantage of this function, we first create a reference to store’s state before action was dispatched (prevState
), then we pass it down with next(action)
, and then we can access state after an action was processed by reducer (nextState
).
The return value from next()
invocation is the state of an action after it got down the middleware chain.
This way we have all the necessary data to see how each action affects our store
Thunk middleware example
Redux-thunk let us return function actions instead of plain objects. Those functions are in most cases focused on making an api calls and dispatching objects with returned data from the server afterwards. Let analyze createThunkMiddleware
source code to see how it’s possible.
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === "function") {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
As you can see, thunkMiddleware
accepts the same parameters as our createLoggerMiddleware
. This is true for every middleware.
Right in the beginning, we check if action is a function, if so – we return a result of invoking it. Often such a function returns a promise that resolves with data returned from back-end API.
extraArgument let us pass some application specific data into our action function. Usually, it’s preconfigured fetch or axios instance.
If action is not a function, thunk just passes it to the next middleware in the chain without doing anything with it at all. It’s a common decision-making pattern among middlewares: decide if action is within the scope of your interest, if so – do something with it. If not, just pass it to the next()
middleware.
How does applyMiddlewares work?
To use middleware with the actual redux store we use the applyMiddleware
function. It let you utilize more than one middleware in your application by creating a chain that will be travelled by dispatched action before it goes to redux’s store dispatch function.
Understanding how this function works is the final step to middleware mastery.
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
);
};
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
};
const chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
};
}
The sole purpose of applyMiddleware is composing the middleware chain with the dispatch method of the Redux store.
As you can see applyMiddleware
creates store enhancer via the createStore function. To create middleware chain from passed ...middlewares
, applyMiddlewares
injects middlewareAPI
, which consists of a reference to getState
and dispatch
method, to each middleware via Array.prototype.map
.
In the end, it composes store dispatch function with the middleware chain that it just created.
The return value is a new, enhanced Redux store that will use the dispatch method enriched by all the logic encapsulated by middlewares.
Summary
Middlewares are powerful functionality that will greatly expand capabilities of your state management workflow. I hope that insight into how actual redux-thunk and applyMiddleware source code works like will helped you to get the deeper understanding of the topic.
It’s important to remember that middlewares are often pretty overwhelming for newcomers, but with some practice, you will feel comfortable creating your own middlewares that will meet the needs of your project.