Redux Toolkit 18 ⚛️ Middleware Serializability — Handling non-serializable data checks
Redux Toolkit’s default middleware includes a serializability check that inspects every dispatched action and every value in the store state. If it finds a Date, a Map, a Set, a class instance, a function, or a Promise, it logs a warning or throws an error depending on the environment. The check exists because Redux DevTools, persistence layers, and time-travel debugging all assume that state and actions are plain JSON-compatible data. When they are not, those tools break silently or produce confusing results.
This chapter covers the serializability check: what it considers serializable, what it flags as not, why the check exists, and the three ways to handle a value that the check rejects—serializableCheck configuration with ignoredActions and ignoredPaths, the serializableCheck: false escape hatch, and the createSerializableStateInvariantMiddleware with custom isSerializable functions. It also covers the legitimate cases where non-serializable data belongs in Redux and the cases where it does not.
Key point: The serializability check is a development-time warning, not a runtime constraint. State and actions still work with non-serializable values. The check exists to warn that DevTools, persistence, and time-travel debugging will not work correctly with those values.
Why the serializability check exists
The DevTools problem. Redux DevTools records every action and every state snapshot. It sends that data to the DevTools extension, which may be in a separate browser frame or a separate process. The transport is postMessage, which uses the structured clone algorithm. Functions, class instances with methods, and certain objects cannot be structured-cloned. A state tree containing a Map or a class instance appears in DevTools as an empty object or throws an error during serialization .
The persistence problem. A state persistence layer like redux-persist serializes the store to localStorage or AsyncStorage using JSON.stringify. A Date becomes a string. A Map becomes {}. A Set becomes {}. A class instance loses its prototype. The rehydrated state does not match the original state, and the application behaves differently after a reload .
The time-travel problem. Time-travel debugging replays actions against previous state snapshots. If an action contains a function reference, the replayed action cannot reproduce the same behavior, because the function is not part of the recorded payload. The replay diverges from the original execution.
The check is a warning system. It tells the developer that the state or action contains a value that will not survive the DevTools transport, the persistence layer, or a time-travel replay.
a. What the check considers serializable
The default isSerializable function considers the following values serializable:
undefined,null,boolean,number,string,bigint- Plain objects (prototype is
Object.prototypeornull) - Arrays
- Objects created by
Immutable.js(handled specially)
Everything else is flagged. That includes Date, Map, Set, WeakMap, WeakSet, Promise, Error, class instances, functions, Symbol, and any object with a custom prototype .
// Serializable
const state = {
count: 1,
name: 'Alice',
tags: ['admin', 'user'],
meta: { active: true },
}
// Not serializable
const state = {
createdAt: new Date(), // Date
cache: new Map(), // Map
tags: new Set(['a', 'b']), // Set
handler: () => {}, // function
user: new User(), // class instance
}
The check runs on every dispatched action and on the state returned by every reducer. It is part of the default middleware stack and is enabled by default in development. In production, the check is disabled for performance .
b. Configuring the check
The serializableCheck option in configureStore accepts either false (disable entirely) or an object with configuration. The object has four useful fields: ignoredActions, ignoredActionPaths, ignoredPaths, and warnAfter.
// store/index.ts
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['auth/login/fulfilled'],
ignoredActionPaths: ['meta.arg', 'payload.timestamp'],
ignoredPaths: ['items.dates', 'cache.entries'],
warnAfter: 100,
},
}),
})
ignoredActions is an array of action types that the check skips entirely. ignoredActionPaths is an array of paths within any action that the check skips. ignoredPaths is an array of paths within the state that the check skips. warnAfter is the threshold in milliseconds after which a slow check produces a warning .
The paths use dot notation and bracket notation: items.dates, cache.entries, payload.items[0].date. A path that points to a Date in the state tells the check to skip that subtree. The rest of the state is still checked.
c. The escape hatch and the trade-offs
Setting serializableCheck: false disables the check entirely. This is the quickest fix, and it is also the one that hides the problem. The state and actions still work. DevTools still records them, but the non-serializable values appear as empty objects or trigger errors in the DevTools extension. Persistence still runs, but the rehydrated state is not equivalent to the original .
The legitimate cases for serializableCheck: false are narrow:
- A development-only state slice that holds a reference to a WebSocket, a
Filehandle, or a non-serializable third-party object that is never persisted and never inspected in DevTools. - A performance-critical action that carries a large non-serializable payload and is dispatched frequently, where the check’s overhead is measurable.
For everything else, the correct fix is to convert the non-serializable value to a serializable one before it enters the store. A Date becomes an ISO string. A Map becomes a plain object or an array of entries. A class instance becomes a plain object with the fields the UI needs. A Set becomes an array. A function reference becomes an ID that the middleware resolves.
Complete Example Session
// ============================================
// PART 1: A NON-SERIALIZABLE ACTION
// ============================================
dispatch({
type: 'events/added',
payload: {
id: '1',
timestamp: new Date(), // Date is not serializable
},
})
// The serializability check warns:
// "A non-serializable value was detected in an action,
// in the path: `payload.timestamp`."
// ============================================
// PART 2: IGNORING A SPECIFIC ACTION PATH
// ============================================
configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActionPaths: ['payload.timestamp'],
},
}),
})
// The check skips payload.timestamp for every action.
// The Date is allowed through.
// ============================================
// PART 3: IGNORING A STATE PATH
// ============================================
configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredPaths: ['events.dates'],
},
}),
})
// The check skips the events.dates subtree in state.
// ============================================
// PART 4: IGNORING A SPECIFIC ACTION TYPE
// ============================================
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['socket/connected'],
},
})
// The entire action is skipped.
// Use this for actions that carry a non-serializable
// reference intentionally.
// ============================================
// PART 5: CONVERTING A DATE TO A STRING
// ============================================
dispatch({
type: 'events/added',
payload: {
id: '1',
timestamp: new Date().toISOString(), // string, serializable
},
})
// The check passes.
// The string can be parsed back to a Date where needed.
// ============================================
// PART 6: CONVERTING A MAP TO AN OBJECT
// ============================================
// Before
const cache = new Map([['a', 1], ['b', 2]])
// After
const cache = { a: 1, b: 2 }
// Or an array of entries
const cache = [['a', 1], ['b', 2]]
// ============================================
// PART 7: CONVERTING A CLASS INSTANCE
// ============================================
// Before
class User {
constructor(public id: string, public name: string) {}
get displayName() { return this.name.toUpperCase() }
}
// After: plain object in the store
const user = { id: '1', name: 'Alice' }
// Compute displayName in a selector
const selectDisplayName = (state) => state.user.name.toUpperCase()
// ============================================
// PART 8: DISABLING THE CHECK ENTIRELY
// ============================================
configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }),
})
// The check never runs.
// DevTools and persistence may break for
// non-serializable values.
// ============================================
// PART 9: CUSTOM isSerializable
// ============================================
import { createSerializableStateInvariantMiddleware } from '@reduxjs/toolkit'
const customCheck = createSerializableStateInvariantMiddleware({
isSerializable: (value) => {
if (value instanceof Date) return true
return (
value === undefined ||
value === null ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
Array.isArray(value) ||
(typeof value === 'object' && value.constructor === Object)
)
},
})
// The custom check treats Date as serializable.
// Use this only if the DevTools and persistence layers
// can handle Date correctly.
// ============================================
// PART 10: THE DECISION TREE
// ============================================
// Is the value persisted? → convert to serializable
// Is it sent to DevTools? → convert to serializable
// Is it time-traveled? → convert to serializable
// Is it a dev-only reference that never leaves memory?
// → ignoredPaths / ignoredActions
// Is the check overhead measurable?
// → serializableCheck: false
The ten parts covered the warning, ignoring action paths, ignoring state paths, ignoring action types, converting a Date, converting a Map, converting a class instance, disabling the check, a custom isSerializable, and the decision tree.
Quick Reference
Serializable values
| Value | Serializable |
|---|---|
undefined, null | Yes |
boolean, number, string, bigint | Yes |
| Plain object | Yes |
| Array | Yes |
Date | No |
Map, Set, WeakMap, WeakSet | No |
Promise | No |
Error | No |
| Class instance | No |
| Function | No |
Symbol | No |
Configuration options
| Option | Type | Purpose |
|---|---|---|
ignoredActions | string[] | Skip entire action types |
ignoredActionPaths | string[] | Skip paths within actions |
ignoredPaths | string[] | Skip paths within state |
warnAfter | number | Warn on slow checks (ms) |
Conversion patterns
| Non-serializable | Serializable replacement |
|---|---|
Date | ISO string |
Map | Plain object or entries array |
Set | Array |
| Class instance | Plain object + selectors |
| Function | ID or action reference |
Escape hatches
| Method | Scope |
|---|---|
ignoredPaths | State subtree |
ignoredActions | Entire action |
ignoredActionPaths | Action path |
serializableCheck: false | Entire store |
Custom isSerializable | Custom logic |
Best Practices
✅ Do This:
// Convert dates to ISO strings before dispatch
timestamp: new Date().toISOString() // ✅
// Convert Maps to plain objects
cache: Object.fromEntries(map) // ✅
// Use ignoredPaths for dev-only references
ignoredPaths: ['socket.connection'] // ✅
// Compute derived values in selectors, not in state
const displayName = (state) => state.user.name.toUpperCase() // ✅
❌ Don’t Do This:
// Store a Date directly
dispatch({ type: 'x', payload: { date: new Date() } }) // ❌
// Store a class instance with methods
dispatch({ type: 'x', payload: new User('1') }) // ❌
// Disable the check to silence a warning
serializableCheck: false // ❌ (hides the problem)
// Store a function reference in state
state.onSave = () => {} // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Warning on Date in action | Date is not serializable | Convert to ISO string |
Warning on Map in state | Map is not serializable | Convert to plain object |
| DevTools shows empty object | Non-serializable value in state | Convert to serializable |
| Persistence breaks after reload | Date or Map serialized incorrectly | Convert before storing |
Warning ignored with false | Check disabled instead of fixed | Use ignoredPaths or convert |
Custom isSerializable too broad | Allowing values that DevTools cannot handle | Keep it narrow |
Real-World Examples
1. Convert a Date
timestamp: new Date().toISOString()
2. Convert a Map
const obj = Object.fromEntries(map)
3. Convert a Set
const arr = [...set]
4. Plain object instead of class
const user = { id: '1', name: 'Alice' }
5. Ignore a state path
ignoredPaths: ['events.dates']
6. Ignore an action type
ignoredActions: ['socket/connected']
7. Ignore an action path
ignoredActionPaths: ['payload.timestamp']
8. Disable the check
getDefaultMiddleware({ serializableCheck: false })
9. Custom isSerializable
createSerializableStateInvariantMiddleware({ isSerializable: (v) => v instanceof Date || defaultCheck(v) })
10. Verify the fix
// Run the app and confirm no serializability warnings
Visual
The check pipeline
┌─────────────────────────────────────────────────────────────┐
│ SERIALIZABILITY CHECK │
│ │
│ action dispatched │
│ │ │
│ ▼ │
│ check action tree │
│ │ │
│ ├── serializable ──► next middleware │
│ │ │
│ └── non-serializable ──► warn/throw │
│ │
│ reducer runs │
│ │ │
│ ▼ │
│ check state tree │
│ │ │
│ ├── serializable ──► done │
│ │ │
│ └── non-serializable ──► warn/throw │
└─────────────────────────────────────────────────────────────┘
What breaks without serializable values
┌─────────────────────────────────────────────────────────────┐
│ WHY IT MATTERS │
│ │
│ DevTools: │
│ Date → appears as string or {} │
│ Map → appears as {} │
│ Class → loses prototype, appears as plain object │
│ │
│ Persistence: │
│ Date → string after reload │
│ Map → {} after reload │
│ Class → plain object after reload │
│ │
│ Time travel: │
│ Function → replay diverges │
└─────────────────────────────────────────────────────────────┘
Configuration options
┌─────────────────────────────────────────────────────────────┐
│ CONFIGURATION │
│ │
│ ignoredActions: ['socket/connected'] │
│ → skip the entire action │
│ │
│ ignoredActionPaths: ['payload.timestamp'] │
│ → skip one path in every action │
│ │
│ ignoredPaths: ['events.dates'] │
│ → skip one subtree in state │
│ │
│ serializableCheck: false │
│ → skip everything │
└─────────────────────────────────────────────────────────────┘
The conversion decision
┌─────────────────────────────────────────────────────────────┐
│ WHAT TO DO WITH A NON-SERIALIZABLE VALUE │
│ │
│ Will it be persisted? │
│ └── Convert to serializable │
│ │
│ Will it appear in DevTools? │
│ └── Convert to serializable │
│ │
│ Is it a dev-only reference (socket, File)? │
│ └── ignoredPaths / ignoredActions │
│ │
│ Is the check overhead measurable? │
│ └── serializableCheck: false │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Check location | Default middleware |
| Enabled in | Development |
| Disabled in | Production (for performance) |
| Serializable | Primitives, plain objects, arrays |
| Not serializable | Date, Map, Set, class, function, Promise |
| Skip action type | ignoredActions |
| Skip action path | ignoredActionPaths |
| Skip state path | ignoredPaths |
| Disable entirely | serializableCheck: false |
| Custom logic | createSerializableStateInvariantMiddleware |
Key takeaways:
- The serializability check is a development-time warning, not a runtime constraint. State and actions still work with non-serializable values. The check warns that DevTools, persistence, and time-travel debugging will not work correctly with those values .
- The default check flags every value that is not a primitive, plain object, or array.
Date,Map,Set, class instances, functions, andPromiseare all flagged. The check runs on every action and every state snapshot . - The correct fix is conversion, not suppression. A
Datebecomes an ISO string. AMapbecomes a plain object. ASetbecomes an array. A class instance becomes a plain object with selectors for derived values. Suppressing the warning hides the problem; conversion eliminates it. ignoredPathsandignoredActionsare for intentional exceptions. A development-only socket reference or aFilehandle that never leaves memory can be excluded with a path. The rest of the state is still checked .serializableCheck: falseis the last resort. It disables the check entirely, which is appropriate only when the overhead is measurable or the entire store is known to contain non-serializable values that never reach DevTools or persistence. For everything else, it is a way to silence a warning without fixing the cause.- A custom
isSerializablefunction is a narrow tool. It allows values that the default check rejects, but it must be kept in sync with what the DevTools and persistence layers can actually handle. ADatethat the custom check allows will still appear as a string in DevTools.
Remember: The serializability check exists because Redux state and actions are expected to be plain data. Plain data can be recorded, replayed, persisted, and inspected. Non-serializable values break those assumptions. The check warns before the breakage reaches production. Convert Date to string, Map to object, Set to array, and class instances to plain objects. Use ignoredPaths for the rare development-only reference that never leaves memory. Use serializableCheck: false only when the check’s overhead is the actual problem, not when it is reporting a real one.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!