| | |

Redux Toolkit 18 ⚛️ Middleware Serializability — Handling non-serializable data checks

⏱️ Estimated read: 11 min | |

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.prototype or null)
  • 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 File handle, 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

ValueSerializable
undefined, nullYes
boolean, number, string, bigintYes
Plain objectYes
ArrayYes
DateNo
Map, Set, WeakMap, WeakSetNo
PromiseNo
ErrorNo
Class instanceNo
FunctionNo
SymbolNo

Configuration options

OptionTypePurpose
ignoredActionsstring[]Skip entire action types
ignoredActionPathsstring[]Skip paths within actions
ignoredPathsstring[]Skip paths within state
warnAfternumberWarn on slow checks (ms)

Conversion patterns

Non-serializableSerializable replacement
DateISO string
MapPlain object or entries array
SetArray
Class instancePlain object + selectors
FunctionID or action reference

Escape hatches

MethodScope
ignoredPathsState subtree
ignoredActionsEntire action
ignoredActionPathsAction path
serializableCheck: falseEntire store
Custom isSerializableCustom 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

PitfallWhy It HappensFix
Warning on Date in actionDate is not serializableConvert to ISO string
Warning on Map in stateMap is not serializableConvert to plain object
DevTools shows empty objectNon-serializable value in stateConvert to serializable
Persistence breaks after reloadDate or Map serialized incorrectlyConvert before storing
Warning ignored with falseCheck disabled instead of fixedUse ignoredPaths or convert
Custom isSerializable too broadAllowing values that DevTools cannot handleKeep 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

ItemValue
Check locationDefault middleware
Enabled inDevelopment
Disabled inProduction (for performance)
SerializablePrimitives, plain objects, arrays
Not serializableDate, Map, Set, class, function, Promise
Skip action typeignoredActions
Skip action pathignoredActionPaths
Skip state pathignoredPaths
Disable entirelyserializableCheck: false
Custom logiccreateSerializableStateInvariantMiddleware

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, and Promise are all flagged. The check runs on every action and every state snapshot .
  • The correct fix is conversion, not suppression. A Date becomes an ISO string. A Map becomes a plain object. A Set becomes an array. A class instance becomes a plain object with selectors for derived values. Suppressing the warning hides the problem; conversion eliminates it.
  • ignoredPaths and ignoredActions are for intentional exceptions. A development-only socket reference or a File handle that never leaves memory can be excluded with a path. The rest of the state is still checked .
  • serializableCheck: false is 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 isSerializable function 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. A Date that 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.

Was this helpful?


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!