Redux Immutable State Invariant Middleware

raw JSON →
2.1.0 verified Thu Apr 23 auth: no javascript

Redux-immutable-state-invariant is a specialized Redux middleware designed exclusively for development environments. Its core function is to detect unintended mutations of the Redux state, both within a reducer's dispatch cycle and between dispatches. This is crucial for upholding the immutability principle central to Redux, preventing subtle bugs that arise from direct state modification rather than returning new state objects. The current stable version is 2.1.0, with a release cadence that has seen significant updates around v1 and v2, and more recently a minor bug fix in v2.1.0, indicating ongoing maintenance. Unlike libraries like Immutable.js which provide immutable data structures, this middleware acts as a runtime check for native JavaScript objects and arrays, throwing descriptive errors when mutations are detected. It is explicitly warned against using in production due to its performance overhead caused by extensive object copying for comparison.

error TypeError: (0 , _reduxImmutableStateInvariant2.default) is not a function
cause Attempting to use `require('redux-immutable-state-invariant')` in a CommonJS environment without accessing the `.default` property for versions 2.0.0 and above.
fix
Modify your require statement to const createImmutableStateInvariantMiddleware = require('redux-immutable-state-invariant').default;.
error Uncaught Error: A state mutation was detected between dispatches. Previous state was { ... }, Next state is { ... }
cause Directly modifying a Redux state object or array property in a reducer or an asynchronous action after the initial state snapshot, rather than returning a new, modified copy.
fix
Ensure all Redux reducers and async logic that modifies state adhere to immutability. Use spread syntax (...) for objects and arrays, or immutable helper libraries, to create new instances of state for any changes. Avoid direct assignments like state.property = value or state.array.push(item).
breaking For CommonJS environments, `require('redux-immutable-state-invariant')` no longer directly returns the middleware factory. You must now access the `.default` property.
fix Change `const createMiddleware = require('redux-immutable-state-invariant');` to `const createMiddleware = require('redux-immutable-state-invariant').default;`.
breaking The middleware factory function (the default export) now accepts a single `options` object as its argument, rather than direct arguments like `isImmutable`.
fix If you were passing `isImmutable` directly, wrap it in an object: `immutableStateInvariantMiddleware(myIsImmutableFn)` becomes `immutableStateInvariantMiddleware({ isImmutable: myIsImmutableFn })`. The `options` object also supports an `ignore` property for paths.
gotcha This middleware is strictly for development environments. Using it in production will severely degrade application performance due to extensive deep copying and comparison of state objects required for mutation detection.
fix Always conditionally apply this middleware based on `process.env.NODE_ENV` or similar environment variables, ensuring it is only included when `NODE_ENV !== 'production'`.
npm install redux-immutable-state-invariant
yarn add redux-immutable-state-invariant
pnpm add redux-immutable-state-invariant

This example demonstrates how to integrate `redux-immutable-state-invariant` into a Redux store, showing both correct immutable updates and how the middleware catches direct state mutations in reducers. It also illustrates how to configure the middleware with `ignore` paths and a custom `isImmutable` function, while emphasizing its development-only usage.

import { applyMiddleware, combineReducers, createStore } from 'redux';
import thunk from 'redux-thunk';
import immutableStateInvariantMiddleware from 'redux-immutable-state-invariant';

const initialState = {
  counter: 0,
  user: { name: 'Alice', age: 30, address: { street: 'Main St' } }
};

function counterReducer(state = initialState.counter, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      // BAD: Mutates state directly, will be caught by middleware
      // state--;
      return state - 1;
    default:
      return state;
  }
}

function userReducer(state = initialState.user, action) {
  switch (action.type) {
    case 'SET_USERNAME':
      // BAD: Mutates state directly, will be caught by middleware
      // state.name = action.payload;

      // GOOD: Returns new state object
      return { ...state, name: action.payload };
    case 'SET_ADDRESS_STREET':
      // BAD: Deep mutation, will be caught
      // state.address.street = action.payload;
      
      // GOOD: Returns new nested state objects
      return { ...state, address: { ...state.address, street: action.payload } };
    default:
      return state;
  }
}

const rootReducer = combineReducers({
  counter: counterReducer,
  user: userReducer
});

// Configure middleware to ignore specific paths or custom immutability checks
const middlewareConfig = {
  ignore: ['user.address.zipCode'], // Example: ignore a specific path
  isImmutable: (value) => {
    // Custom check: treat anything with a '__immutable' property as immutable
    if (typeof value === 'object' && value !== null && value.__immutable) {
      return true;
    }
    // Default check for primitives
    return typeof value !== 'object' || value === null;
  }
};

const middleware = process.env.NODE_ENV !== 'production' ?
  [immutableStateInvariantMiddleware(middlewareConfig), thunk] :
  [thunk];

const store = createStore(
  rootReducer,
  applyMiddleware(...middleware)
);

console.log('Initial state:', store.getState());

store.dispatch({ type: 'INCREMENT' });
console.log('State after INCREMENT:', store.getState());

store.dispatch({ type: 'SET_USERNAME', payload: 'Bob' });
console.log('State after SET_USERNAME:', store.getState());

// Intentionally trigger a mutation (uncomment to see the error in development)
// store.dispatch({
//   type: 'MUTATE_COUNTER_BADLY',
//   payload: store.getState().counter++
// });