How to Implement Redux Middleware in React

Redux middleware provides a powerful way to extend Redux’s capabilities by intercepting actions before they reach the reducer. This article explains how to implement Redux middleware in a React application, covering the integration of third-party middleware, configuring modern Redux Toolkit, and writing your own custom middleware from scratch.

What is Redux Middleware?

In Redux, middleware sits between the moment an action is dispatched and the moment it reaches the reducer. It is commonly used for side effects, such as asynchronous API calls (using redux-thunk), logging state changes (using redux-logger), or routing.

Implementing Middleware with Redux Toolkit

Redux Toolkit (RTK) is the recommended way to write Redux logic today. It simplifies store setup and automatically includes default middleware like redux-thunk for asynchronous operations.

To add custom or third-party middleware to your Redux Toolkit store, use the middleware option in configureStore.

import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
import logger from 'redux-logger'; // Example of third-party middleware

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => 
    getDefaultMiddleware().concat(logger),
});

export default store;

In this setup, getDefaultMiddleware() returns an array containing the default middleware provided by RTK. The .concat() method is used to safely append new middleware without mutating the default array.

Writing Custom Middleware

Sometimes you need to write custom logic to handle specific actions. A Redux middleware is structured as a series of three nested functions (often referred to as a curried function):

  1. Outer function: Receives the store’s dispatch and getState methods.
  2. Middle function: Receives the next dispatch function.
  3. Inner function: Receives the action being dispatched.

Here is an example of a custom logging middleware:

const customLoggerMiddleware = (store) => (next) => (action) => {
  console.log('Dispatching action:', action);
  
  // Pass the action to the next middleware or reducer
  const result = next(action);
  
  console.log('Next state:', store.getState());
  
  return result;
};

export default customLoggerMiddleware;

To implement this custom middleware in your Redux Toolkit store:

import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
import customLoggerMiddleware from './customLoggerMiddleware';

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => 
    getDefaultMiddleware().concat(customLoggerMiddleware),
});

export default store;

Implementing Middleware in Legacy Redux (Using applyMiddleware)

If you are working with a legacy codebase that uses the core redux package instead of Redux Toolkit, you must use the applyMiddleware function from the redux library.

import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducer';
import thunk from 'redux-thunk';
import customLoggerMiddleware from './customLoggerMiddleware';

const store = createStore(
  rootReducer,
  applyMiddleware(thunk, customLoggerMiddleware)
);

export default store;

Connecting the Store to your React Application

Once the store is configured with your middleware, connect it to your React components using the Provider component from the react-redux library.

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

By wrapping your application in the Provider, any action dispatched from your React components using useDispatch will now pass through your implemented middleware before updating the store.