Skip to main content

Serializable Props in React: Tutorial

When a server component passes data to a client component via props, that data must be serializable—convertible to JSON and back. Functions, class instances, symbols, and dates cannot cross the server-client boundary directly. This is because the data travels over the RSC Payload, which is a JSON-like serialized format. Understanding serialization rules is essential for avoiding runtime errors and designing correct data-passing patterns.

This article teaches you exactly what is and isn't serializable, shows you practical workarounds, and demonstrates patterns for handling complex data types that need to flow from server to client.

What Is Serializable Data?

Serializable data can be encoded as JSON and reconstructed on the other side. The JSON specification supports:

  • Primitives: strings, numbers, booleans, null
  • Collections: arrays, plain objects (key-value pairs)

Non-serializable values include:

  • Functions: cannot be encoded as JSON
  • Class instances: lose their methods during serialization
  • Symbols: unique and cannot be serialized
  • Dates: become strings; the type information is lost
  • Map, Set, WeakMap, WeakSet: not JSON-compatible
  • Undefined: omitted in JSON
  • BigInt: not supported by JSON.stringify
// ✅ Serializable
const serializable = {
name: 'Alice',
age: 30,
tags: ['react', 'javascript'],
active: true,
metadata: { created: '2026-06-02' }, // date as string, not Date object
};

// ❌ Non-serializable
const nonSerializable = {
name: 'Bob',
handler: () => console.log('hi'), // function
created: new Date(), // Date object
cache: new Map([['key', 'value']]), // Map
id: Symbol('unique'), // Symbol
};

When you pass non-serializable data from a server component to a client component as a prop, Next.js will throw an error at runtime, telling you the field cannot be serialized.

How Server Components Serialize Props

When a server component renders a client component, Next.js serializes the props into the RSC Payload. The serialization happens at the component boundary:

// app/page.tsx — server component
import { UserCard } from './user-card';

async function HomePage() {
const user = await db.users.findById(1);

return (
<UserCard
id={user.id}
name={user.name}
joinedAt={user.joinedAt} // Date object from database
role={user.role}
/>
);
}

export default HomePage;

If user.joinedAt is a JavaScript Date object, serialization will fail or convert it to a string, losing type information. To pass a date safely, convert it to a string or timestamp on the server:

// ✅ Correct: convert Date to string before passing as prop
async function HomePage() {
const user = await db.users.findById(1);

return (
<UserCard
id={user.id}
name={user.name}
joinedAt={user.joinedAt.toISOString()} // Serialize Date to ISO string
role={user.role}
/>
);
}

The client component receives joinedAt as a string and can reconstruct a Date if needed:

// components/user-card.tsx — client component
'use client';

export function UserCard({ id, name, joinedAt, role }) {
const date = new Date(joinedAt); // Reconstruct from ISO string

return (
<div>
<h2>{name}</h2>
<p>Joined: {date.toLocaleDateString()}</p>
<p>Role: {role}</p>
</div>
);
}

Handling Complex Objects

For plain objects (no methods, just data), serialization is automatic. But if your database returns an object with methods or getters, you must extract only the data fields:

// Database returns a User object with methods
const user = await db.users.findById(1);
// user is an instance of User class with getters and methods
// user.getDisplayName(), user.isAdmin(), etc.

// ❌ Cannot pass the object directly; it has methods
// <UserDisplay user={user} />

// ✅ Extract data into a plain object
const userData = {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
};

return <UserDisplay user={userData} />;

In TypeScript, use Pick or create a type to enforce this pattern:

type UserData = Pick<User, 'id' | 'name' | 'email' | 'role'>;

async function HomePage() {
const user = await db.users.findById(1);

const userData: UserData = {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
};

return <UserDisplay user={userData} />;
}

Common Serialization Pitfalls

Pitfall 1: Passing functions as props

// ❌ Error: cannot serialize function
async function Parent() {
const handleClick = () => console.log('clicked');
return <Child onClick={handleClick} />;
}

Solution: If you need a callback, use an API route or server action. Server actions (a Next.js feature) allow you to call server functions from client components safely without sending function code to the client.

// ✅ Server action: callable from client, serializable
'use server';

export async function logClick() {
console.log('clicked on server');
// Access database, secrets, etc.
}
// Client component calls the server action
'use client';

import { logClick } from './actions';

export function Child() {
return <button onClick={() => logClick()}>Click</button>;
}

Pitfall 2: Passing undefined

// ❌ undefined is omitted in JSON
const data = {
name: 'Alice',
nickname: undefined,
age: 30,
};

// After serialization/deserialization, nickname key is gone

Solution: Use null instead of undefined for missing values:

const data = {
name: 'Alice',
nickname: null, // ✅ Serializes correctly
age: 30,
};

Pitfall 3: Dates and timestamps

// ❌ Date object loses type information
const user = { name: 'Bob', created: new Date() };
// In the client, created becomes a string like "2026-06-02T10:30:00.000Z"

Solution: Explicitly convert to ISO string or Unix timestamp:

const user = {
name: 'Bob',
createdAt: new Date().getTime(), // Unix timestamp (number)
// or
createdAt: new Date().toISOString(), // ISO string
};

Using React Query or SWR with Serializable Data

If you're using React Query or SWR (which run on the client), you still need to fetch and serialize data on the server first, or pass the serialized data from the server component:

// Server component fetches and serializes
async function PostList() {
const posts = await db.posts.findAll();

const serializedPosts = posts.map(post => ({
id: post.id,
title: post.title,
publishedAt: post.publishedAt.toISOString(),
}));

return <ClientPostList initialPosts={serializedPosts} />;
}
// Client component
'use client';

import { useState } from 'react';

export function ClientPostList({ initialPosts }) {
const [posts, setPosts] = useState(initialPosts);

// Refresh data on client
const refetch = async () => {
const res = await fetch('/api/posts');
setPosts(await res.json());
};

return (
<div>
<button onClick={refetch}>Refresh</button>
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{new Date(post.publishedAt).toLocaleDateString()}</p>
</li>
))}
</ul>
</div>
);
}

Key Takeaways

  • Serializable data includes primitives (strings, numbers, booleans), arrays, and plain objects. Non-serializable data includes functions, class instances, Dates, symbols, and collections like Map and Set.
  • When passing data from server to client components, convert non-serializable types to serializable equivalents (Dates to ISO strings, class instances to plain objects).
  • Use null instead of undefined for optional fields to ensure they serialize correctly.
  • For functions and callbacks that need to cross the boundary, use server actions (Next.js feature) instead of passing function props.
  • Extract only the data fields you need from database objects before passing them as props to client components.

Frequently Asked Questions

Why can't I pass a function from a server component to a client component?

Functions are not serializable to JSON. The server-client boundary requires all data to be transmitted as JSON. For server-side logic, use server actions (a Next.js feature) that the client can call remotely instead of passing function references.

Can I pass a Date object if I convert it to a string first?

Yes. Convert the Date to an ISO string using toISOString() or to a Unix timestamp using getTime(). The client receives the string or number and can reconstruct a Date object if needed.

What if my database ORM returns objects with methods?

Extract only the data properties into a plain object before passing as a prop. Use TypeScript's Pick utility or create a DTO (Data Transfer Object) type to define which fields to include.

How do I handle nested objects with dates?

Recursively convert all Date fields to strings. For complex nested structures, write a helper function to traverse and serialize the entire object tree before passing it as a prop.

Can I use JSON.stringify and JSON.parse to serialize custom objects?

JSON.stringify will omit non-serializable fields (functions, undefined, symbols), and JSON.parse will lose type information for dates and other objects. Explicitly convert non-serializable types before stringifying, and reconstruct them after parsing.

Further Reading