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.

June 28, 20263 min read4 / 4

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:

JavaScript
const myLogger = (store) => (next) => (action) => { console.log('Custom middleware executed'); return next(action); };
  • store: the Redux store instance, with getState and dispatch
  • next: the next middleware in the chain (or the real dispatch if 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:

JavaScript
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:

Bash
npm install uuid

Import v1 (timestamp and MAC address based, globally unique) and use it in the middleware:

JavaScript
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):

JSX
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.

Action intercepted by custom middleware before reaching the reducer 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

  1. Three nested functions: (store) => (next) => (action) => { ... }. The innermost function is where the logic runs. Always call next(action) to continue the chain.
  2. Middleware order determines the next target. The first middleware in applyMiddleware runs first. Its next is the second middleware. The last middleware's next is the real store dispatch.
  3. Payload transformation is safe: mutate action.payload before calling next(action) and the modified action flows to all subsequent middleware and the reducer.

Further Reading and Watching