Redux Immutable State Invariant Middleware
raw JSON →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.
Common errors
error TypeError: (0 , _reduxImmutableStateInvariant2.default) is not a function ↓
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 { ... } ↓
...) 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). Warnings
breaking For CommonJS environments, `require('redux-immutable-state-invariant')` no longer directly returns the middleware factory. You must now access the `.default` property. ↓
breaking The middleware factory function (the default export) now accepts a single `options` object as its argument, rather than direct arguments like `isImmutable`. ↓
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. ↓
Install
npm install redux-immutable-state-invariant yarn add redux-immutable-state-invariant pnpm add redux-immutable-state-invariant Imports
- immutableStateInvariantMiddleware wrong
import { immutableStateInvariantMiddleware } from 'redux-immutable-state-invariant';correctimport immutableStateInvariantMiddleware from 'redux-immutable-state-invariant'; - createImmutableStateInvariantMiddleware wrong
const createImmutableStateInvariantMiddleware = require('redux-immutable-state-invariant');correctconst createImmutableStateInvariantMiddleware = require('redux-immutable-state-invariant').default; - MiddlewareFactoryWithOptions wrong
immutableStateInvariantMiddleware((val) => typeof val !== 'object');correctimmutableStateInvariantMiddleware({ isImmutable: (val) => typeof val !== 'object', ignore: ['users.drafts'] });
Quickstart
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++
// });