Custom Redux Middleware: Intercepting and Transforming Actions
Custom middleware lets you intercept every dispatched action before it reaches the reducer. Use it to log, validate, transform payloads, or generate IDs without touching component or action creator code.
Redux Thunk and Redux Logger are both middleware. They sit between dispatch and the reducer, intercept actions, and do something with them. Writing your own middleware follows the same structure.
You rarely need one in production since third-party middleware covers most common cases. But knowing how to write one clarifies what Thunk and Logger are actually doing internally.
The Middleware Signature
A Redux middleware is a curried function with three layers:
const myLogger = (store) => (next) => (action) => {
console.log('Custom middleware executed');
return next(action);
};store: the Redux store instance, withgetStateanddispatchnext: the next middleware in the chain (or the realdispatchif this is the last one)action: the action object dispatched by the component
Calling next(action) is required. It passes the action to the next middleware or reducer. Omitting it swallows the action silently.
Register the middleware in the store setup as the first argument in applyMiddleware:
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import allReducers from '../reducers';
const myLogger = (store) => (next) => (action) => {
console.log('store:', store.getState());
console.log('next:', next);
console.log('action:', action);
return next(action);
};
const store = createStore(
allReducers,
composeWithDevTools(applyMiddleware(myLogger, thunk, logger))
);The order matters. As the first middleware, myLogger's next parameter points to Redux Thunk. Thunk's next points to Logger. Logger's next points to the real dispatch. Each middleware calls the next one in sequence.
Transforming the Action Payload
Middleware can modify the action before passing it forward. A real use case: generating a UUID for every created task inside middleware instead of inside the component.
Install the uuid package:
npm install uuidImport v1 (timestamp and MAC address based, globally unique) and use it in the middleware:
import { v1 as uuidv1 } from 'uuid';
import * as actionTypes from '../constants/action-types';
const uuidMiddleware = (store) => (next) => (action) => {
if (action.type === actionTypes.CREATE_TASK_REQUEST) {
action.payload.id = uuidv1();
}
return next(action);
};When CREATE_TASK_REQUEST is dispatched with a newTask payload, this middleware injects a UUID as payload.id before the action reaches the reducer. The component and action creator stay unchanged. The reducer receives a payload that already has a unique, production-quality id.
The component's onSaveClick no longer needs Math.floor(Math.random() * 1_000_000):
function onSaveClick() {
const newTask = { taskTitle, dateTime }; // no id needed
dispatch(actions.createTask(newTask));
}The middleware handles id generation as a cross-cutting concern, separate from the component.
ExpandAction intercepted by custom middleware before reaching the reducer
When to Use Custom Middleware
Common real-world uses:
- Payload transformation: inject ids, timestamps, or metadata into actions before they hit the reducer
- Analytics: fire analytics events on specific action types without touching component code
- Access control: cancel or redirect actions based on current state (e.g., block an action when the user is unauthenticated)
- Error logging: catch unhandled action errors in the middleware chain
For logging and async operations, existing packages (redux-logger, redux-thunk) cover the cases. Build custom middleware when those packages do not fit the requirement.
The Essentials
- Three nested functions:
(store) => (next) => (action) => { ... }. The innermost function is where the logic runs. Always callnext(action)to continue the chain. - Middleware order determines the
nexttarget. The first middleware inapplyMiddlewareruns first. Itsnextis the second middleware. The last middleware'snextis the real store dispatch. - Payload transformation is safe: mutate
action.payloadbefore callingnext(action)and the modified action flows to all subsequent middleware and the reducer.
Further Reading and Watching
- Redux middleware docs: the official explanation of how middleware composition works
- uuid on npm: v1 vs v4 trade-offs and usage
Keep reading