Why Redux Doesn't Allow Non-Serializable Data (And How to Work Around It) 🔄

Last month, while building a personal file upload component for my project, I hit a wall that left me scratching my head. I was trying to store a File object directly in my Redux store, and suddenly my browser console exploded with warnings about "non-serializable data." At first, I thought it was just Redux being unnecessarily strict, but as I dug deeper, I discovered there were solid architectural reasons behind this design decision.

This article explores why Redux enforces serialization, the problems non-serializable data causes, and practical workarounds when you absolutely need to handle files, functions, or other complex objects.


The Problem: My File Upload Nightmare

Picture this: I'm building a document management feature where users can upload multiple files, preview them, and submit them with additional metadata. My initial Redux action looked something like this:

// ❌ This seemed logical at first...
const uploadFile = (file) => ({
	type: "UPLOAD_FILE",
	payload: {
		file: file, // File object from input element
		uploadedAt: new Date(),
		id: Math.random(),
	},
});

The moment I dispatched this action, Redux DevTools lit up with warnings:

A non-serializable value was detected in an action, in the path: `payload.file`.
Value: File { name: "document.pdf", size: 1024, type: "application/pdf" }

My first instinct was to ignore it—after all, the app seemed to work fine. But then I tried using Redux DevTools' time-travel debugging, and everything broke. The file previews disappeared, the upload progress reset, and I realized I had stumbled into a fundamental Redux principle I didn't fully understand.


What Does "Serializable" Actually Mean?

Serialization is the process of converting complex data structures into a format that can be stored, transmitted, or reconstructed later. In JavaScript, this typically means converting objects into JSON strings.

Serializable vs Non-Serializable Data

Serializable data examples:

// âś… These can be safely converted to JSON
const serializable = {
	string: "hello",
	number: 42,
	boolean: true,
	array: [1, 2, 3],
	object: { nested: "value" },
	null: null,
};

JSON.stringify(serializable); // Works perfectly

Non-serializable data examples:

// ❌ These cannot be converted to JSON without losing information
const nonSerializable = {
	file: new File(["content"], "test.txt"),
	date: new Date(),
	function: () => console.log("hello"),
	undefined: undefined,
	symbol: Symbol("unique"),
	regex: /pattern/g,
	map: new Map([["key", "value"]]),
	set: new Set([1, 2, 3]),
};

JSON.stringify(nonSerializable);
// Result: {"date":"2025-06-28T10:00:00.000Z","regex":{},"map":{},"set":{}}
// Notice how much information is lost!

Why Redux Enforces Serialization: The Deep Reasons

After researching Redux's design philosophy, I discovered several compelling reasons for this restriction:

1. Time-Travel Debugging

Redux DevTools' most powerful feature is the ability to "time travel"—replay actions and inspect state at any point. This requires storing the complete state history.

// Redux DevTools needs to do this internally:
const stateHistory = [
	JSON.parse(JSON.stringify(state1)), // Snapshot at time 1
	JSON.parse(JSON.stringify(state2)), // Snapshot at time 2
	JSON.parse(JSON.stringify(state3)), // Snapshot at time 3
];

When non-serializable data is involved, these snapshots become incomplete or corrupted. My File objects turned into empty objects {}, making time-travel debugging useless.

2. Predictable State Mutations

Redux's core principle is that state should be updated immutably. Non-serializable objects often contain mutable properties that can change without dispatching actions:

// ❌ Dangerous: File objects can be modified externally
const state = {
	uploadedFile: someFileObject, // This object might change!
};

// Somewhere else in the code:
someFileObject.name = "modified.txt"; // State changed without Redux knowing!

3. Persistence and Hydration

Many Redux applications need to persist state to localStorage or send it over the network. Non-serializable data breaks this completely:

// ❌ This fails silently
localStorage.setItem("reduxState", JSON.stringify(stateWithFiles));

// Later...
const restoredState = JSON.parse(localStorage.getItem("reduxState"));
// Files are now empty objects!

4. Server-Side Rendering (SSR)

In SSR applications, the Redux store needs to be serialized on the server and hydrated on the client. Non-serializable data makes this impossible:

// Server-side
const initialState = store.getState();
const serializedState = JSON.stringify(initialState); // Breaks with non-serializable data

// Client-side
const clientStore = createStore(reducer, JSON.parse(serializedState)); // Incomplete state

Real-World Consequences I Discovered

Through my file upload project, I experienced these problems firsthand:

Hot Reloading Breaks

During development, hot reloading would clear my file previews because the File objects couldn't be preserved across reloads.

Redux DevTools Become Useless

I couldn't debug my upload flow because the file data disappeared in the DevTools history.

State Persistence Fails

When I tried to implement "draft" functionality (saving user progress), the files were lost during state rehydration.

Testing Becomes Difficult

Snapshot testing broke because File objects don't serialize consistently across test runs.


Practical Solutions: How to Handle Non-Serializable Data

After understanding the problems, I developed several strategies for working with files and other non-serializable data:

Solution 1: Store References, Not Objects

Instead of storing the actual File object, store a reference and keep the file elsewhere:

// âś… Store file metadata and references
const uploadFile = (file) => {
	const fileId = generateUniqueId();

	// Store file in a separate manager
	FileManager.store(fileId, file);

	return {
		type: "UPLOAD_FILE",
		payload: {
			fileId,
			name: file.name,
			size: file.size,
			type: file.type,
			uploadedAt: Date.now(), // Store timestamp instead of Date object
		},
	};
};

// Retrieve file when needed
const getFile = (fileId) => FileManager.get(fileId);

Solution 2: Convert to Serializable Formats

For files, convert them to base64 strings or ArrayBuffers (though be mindful of memory usage):

// âś… Convert file to base64 for small files
const convertFileToBase64 = (file) => {
	return new Promise((resolve) => {
		const reader = new FileReader();
		reader.onload = () => resolve(reader.result);
		reader.readAsDataURL(file);
	});
};

const uploadFile = async (file) => {
	const base64Data = await convertFileToBase64(file);
	return {
		type: "UPLOAD_FILE",
		payload: {
			name: file.name,
			size: file.size,
			type: file.type,
			data: base64Data, // Now serializable!
		},
	};
};

Solution 3: Use Redux Middleware to Filter Non-Serializable Data

Redux Toolkit provides middleware that can automatically handle or warn about non-serializable data:

import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({
	reducer: rootReducer,
	middleware: (getDefaultMiddleware) =>
		getDefaultMiddleware({
			serializableCheck: {
				ignoredActions: ["file/upload"], // Ignore specific actions
				ignoredPaths: ["files.tempUploads"], // Ignore specific state paths
				// Or disable entirely (NOT recommended)
				// serializableCheck: false
			},
		}),
});

Solution 4: Separate Non-Serializable State

Keep non-serializable data in React component state or context, and only store serializable metadata in Redux:

// Component handles file objects
const FileUpload = () => {
	const [files, setFiles] = useState([]); // Non-serializable files stay here
	const dispatch = useDispatch();

	const handleUpload = (file) => {
		setFiles((prev) => [...prev, file]); // Store file in component state

		dispatch(
			addFileMetadata({
				// Only metadata goes to Redux
				id: generateId(),
				name: file.name,
				size: file.size,
				status: "uploading",
			})
		);
	};

	// ... rest of component
};

When Is It OK to Break the Rules?

Sometimes, you might need to bypass Redux's serialization requirements. Here are scenarios where it might be acceptable:

Temporary UI State

// For short-lived, non-critical data
const store = configureStore({
	reducer: rootReducer,
	middleware: (getDefaultMiddleware) =>
		getDefaultMiddleware({
			serializableCheck: {
				ignoredPaths: ["ui.tempFiles"], // Only ignore specific paths
			},
		}),
});

Development-Only Features

// Disable checks only in development
const store = configureStore({
	reducer: rootReducer,
	middleware: (getDefaultMiddleware) =>
		getDefaultMiddleware({
			serializableCheck: process.env.NODE_ENV === "production",
		}),
});

Key Takeaways

Redux's serialization requirement isn't arbitrary—it's fundamental to its debugging, persistence, and predictability features. While it initially seemed like a roadblock in my file upload project, understanding the reasoning led me to build a more robust, maintainable solution.

The main lessons I learned:

  1. Respect the architecture: Redux's constraints exist for good reasons
  2. Separate concerns: Keep non-serializable data separate from your Redux state
  3. Store references, not objects: Use IDs and external managers for complex data
  4. Convert when possible: Transform non-serializable data into serializable formats
  5. Break rules carefully: If you must bypass serialization, do it selectively and document why

Understanding these principles has made me a better Redux developer and helped me build more maintainable applications. The next time you encounter that serialization warning, don't ignore it—embrace it as an opportunity to build better architecture! 🚀