A common Redux anti-pattern is storing non-serializable values in the Redux store, which can lead to issues with time-travel debugging, persistence, and general maintainability. Here’s how to properly handle complex data in Redux.
Why Serializability Matters
- Debugging: Redux DevTools relies on serializable actions and state
- Persistence: Needed for saving state to storage and rehydrating
- Predictability: Ensures consistent behavior across environments
- Middleware: Many Redux middleware expect serializable data
Problematic Data Types to Avoid
❌ Class instances
❌ Functions/Promises
❌ Symbols
❌ Maps/Sets
❌ React components
❌ Circular references
❌ Native DOM elements
Solutions for Common Cases
1. Handling Class Instances
Instead of:
// ❌ Bad - storing class instance
state.user = new User(userData);
Do:
// ✅ Good - store plain objects
state.user = userData;
// If needed, reconstruct when selecting
const selectUser = (state) => new User(state.user);
2. Dealing with Functions
Instead of:
// ❌ Bad - storing function in state
state.onComplete = () => console.log('Done!');
Do:
// ✅ Good - use action types/thunks
// Dispatch this action when complete
dispatch({ type: 'TASK_COMPLETED' });
3. Managing Dates
Instead of:
// ❌ Bad - Date object isn't fully serializable
state.lastUpdated = new Date();
Do:
// ✅ Good - store as ISO string
state.lastUpdated = new Date().toISOString();
// Convert back when needed
new Date(state.lastUpdated);
4. Handling Complex Objects
Instead of:
// ❌ Bad - Map instance
state.items = new Map([[1, 'a'], [2, 'b']]);
Do:
// ✅ Good - convert to plain object/array
state.items = { 1: 'a', 2: 'b' };
// or
state.items = [[1, 'a'], [2, 'b']];
Redux Toolkit Best Practices
Redux Toolkit’s configureStore
automatically checks for non-serializable values:
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these action types
ignoredActions: ['some/non-serializable'],
// Ignore these paths in the state
ignoredPaths: ['items.dates'],
},
}),
});
When You Must Store Non-Serializable Data
For legitimate cases where you need non-serializable values:
- Document why it’s necessary
- Configure middleware to ignore those paths
- Consider alternative approaches:
- Store in component state instead
- Use React context
- Utilize a specialized library (like Redux-ORM)
Migration Strategy
- Audit your store for non-serializable values
- Refactor incrementally – start with most problematic areas
- Add serializability checks to catch new issues
- Educate your team about the importance
Debugging Non-Serializable Values
Redux Toolkit will log warnings in development. You can also add custom checks:
function checkSerializable(value) {
try {
JSON.stringify(value);
return true;
} catch (e) {
console.warn('Non-serializable value:', value);
return false;
}
}
By maintaining serializable state, you’ll ensure your Redux store works reliably with all the ecosystem tools and follows established best practices.