How to Fix 'Redux: A Non-Serializable Value Was Detected in the State' (Root Cause + Refactor)
Bug Severity: HIGH | UX/User Impact: SEVERE (broken DevTools, failed state hydration, silent SSR mismatch) | Time to Fix: 10–20 mins
TL;DR
- What broke: A non-serializable value — a class instance,
Dateobject,Map,Set,Function, orPromise— was written into Redux state, violating the Redux serialization contract. - How to fix it: Replace the offending value with a plain serializable equivalent (ISO string instead of
Date, plain object instead of class instance, array of tuples instead ofMap). - Shortcut: Use our Client-Side Sandbox above to paste your reducer or slice — it will auto-detect the non-serializable field and generate the refactored code instantly.
The Incident (What Does the Error Mean?)
The raw warning thrown by @reduxjs/toolkit or redux-immutable-state-invariant middleware:
A non-serializable value was detected in the state, in the path: `someSlice.user.createdAt`.
Value: Wed Jun 11 2025 10:22:00 GMT+0000
Take a look at the reducer(s) handling this action type: user/setProfile.
Immediate consequences:
- Redux DevTools time-travel breaks silently — replaying actions produces inconsistent state.
redux-persistfails to serialize the store tolocalStorage, corrupting persisted state on reload.- SSR hydration mismatches: server-serialized state (JSON) cannot round-trip a
Dateor class instance, so client rehydration produces a different object reference, triggering React hydration errors. JSON.stringify(store.getState())either throws or silently drops the field.
The Attack Vector / Blast Radius
This is not a single-component bug. The blast radius scales with how central the offending slice is:
Scenario 1 — Date object in state:
Every component subscribed to that slice re-renders with an object that looks correct in the UI but cannot be serialized, persisted, or replayed. QA never catches it because the app renders fine in a fresh session. It detonates the first time a user refreshes with persisted state or a developer tries to replay a bug via DevTools.
Scenario 2 — Class instance (e.g., new UserModel()) in state:
Class instances carry prototype methods. After JSON.stringify → JSON.parse (persist, SSR, postMessage to a Web Worker), the prototype chain is stripped. The rehydrated object is a plain object missing all methods. Any call to state.user.getFullName() throws TypeError: state.user.getFullName is not a function — in production, after a page reload, for every user.
Scenario 3 — Map or Set in state:
JSON.stringify(new Map([['a', 1]])) produces {}. The entire data structure silently vanishes on persist/rehydrate. If this is a permissions map or feature-flag set, you've just silently dropped authorization data.
Scenario 4 — Function or Promise in state:
Functions are not data. Storing a callback or thunk in state is an architectural error that makes the store non-testable and non-serializable by definition.
How to Fix It (The Solution)
Basic Fix — Replace Non-Serializable Types
Date object → ISO string:
// userSlice.js
const userSlice = createSlice({
name: 'user',
initialState: {
- createdAt: new Date(),
+ createdAt: new Date().toISOString(),
},
reducers: {
setProfile: (state, action) => {
- state.createdAt = action.payload.createdAt; // payload is a Date object
+ state.createdAt = action.payload.createdAt instanceof Date
+ ? action.payload.createdAt.toISOString()
+ : action.payload.createdAt;
},
},
});
Class instance → plain object:
- import { UserModel } from './models/UserModel';
setUser: (state, action) => {
- state.user = new UserModel(action.payload);
+ state.user = {
+ id: action.payload.id,
+ name: action.payload.name,
+ email: action.payload.email,
+ };
}
Map → plain object or array of tuples:
initialState: {
- permissions: new Map([['admin', true], ['editor', false]]),
+ permissions: { admin: true, editor: false },
}
Enterprise Best Practice — Enforce Serialization at the Middleware Layer
Don't rely on developers remembering. Enforce it in the store configuration.
// store.js
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: rootReducer,
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ serializableCheck: {
+ // Whitelist only if you have a documented, justified exception (e.g., redux-form)
+ ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
+ // NEVER ignore entire state paths without a written ADR
+ // ignoredPaths: ['someSlice.dangerousField'], // <-- do not do this blindly
+ warnAfter: 32, // ms threshold before warning fires
+ },
+ }),
});
If you are using redux-persist, the persist/PERSIST and persist/REHYDRATE actions carry non-serializable transforms internally. Whitelist only those specific actions, not entire state paths.
For class-heavy domains (e.g., you receive API responses as class instances from an ORM or SDK): normalize at the boundary — in the thunk or RTK Query transformResponse — before the data ever touches the store.
// api slice (RTK Query)
transformResponse: (response) => {
- return response.data; // response.data is a class instance from the SDK
+ return {
+ id: response.data.id,
+ name: response.data.name,
+ status: response.data.status,
+ };
}
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. ESLint rule — redux-immutable-state-invariant in test environments:
Ensure NODE_ENV=test runs with the full middleware stack so Jest catches serialization violations before they reach a PR.
# .env.test
REDUX_SERIALIZE_CHECK=true
2. Custom ESLint rule or eslint-plugin-redux-saga / manual lint:
Ban new Date(), new Map(), new Set() inside reducer files and slice initialState definitions.
// .eslintrc
{
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "NewExpression[callee.name='Date']",
"message": "Do not use 'new Date()' in Redux state. Use Date.now() or new Date().toISOString() and store as a number or string."
}
]
}
}
3. RTK serializableCheck set to throw in CI:
By default the middleware only warns. In your test environment, override it to throw:
// store.test.ts
const store = configureStore({
reducer: rootReducer,
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ serializableCheck: {
+ isSerializable: (value) => {
+ const result = isPlain(value);
+ if (!result) throw new Error(`Non-serializable value in Redux state: ${typeof value}`);
+ return result;
+ },
+ },
+ }),
});
This turns silent production warnings into hard CI failures on the branch that introduced them.
4. Architecture Decision Record (ADR):
Any ignoredPaths or ignoredActions added to serializableCheck must be accompanied by a committed ADR explaining the exception, the risk accepted, and the owner. This prevents the whitelist from becoming a graveyard of forgotten hacks.