Next.js RSC Serialization Limits
What can (and cannot) cross the server-client boundary in React Server Components
React Server Components serialize their rendered output and pass it to the browser over the network. This serialization requirement creates strict boundaries on what data you can pass from server to client. The key insight: only JSON-serializable data can cross the RSC boundary. Functions, classes, and complex objects silently fail with cryptic errors.
The Serialization Boundary
When a Server Component renders, Next.js uses React Server Components serialization to convert the component tree into a format that can be transmitted over HTTP and rendered in the browser. This process has strict rules:
// ✅ Serializable - works fine
export default async function ServerComponent() {
const data = {
string: "hello",
number: 42,
array: [1, 2, 3],
object: { nested: { value: true } },
null: null,
date: new Date().toISOString() // Stringified date
};
returnThe serialization happens at the boundary between Server Component and Client Component. If you import and use a Client Component inside a Server Component, any props you pass must be serializable.
What Can Be Serialized
Primitives and Plain Objects
JSON-serializable types work without issues:
- Strings, numbers, booleans, null, undefined - Primitive types
- Arrays - Containing serializable values
- Plain objects - With serializable properties
- Date strings -
date.toISOString()(not Date objects)
// ✅ Correct date handling
export default async function ServerComponent() {
const post = await db.post.findFirst();
returnServer Actions and Event Handlers
Starting with Next.js 14, you can pass Server Actions as props to Client Components:
// ✅ Server Actions are serializable
import { createUser } from '@/app/actions';
export default function ServerComponent() {
returnServer Actions are serialized as special references that Next.js can re-hydrate on the client. Regular functions are not serializable.
What Cannot Be Serialized
Functions (Except Server Actions)
Regular functions cannot be serialized because they cannot be converted to JSON:
// ❌ Error: Functions cannot be serialized
export default function ServerComponent() {
const handleClick = () => console.log("clicked");
returnThe error message is misleading: "Objects are not valid as a React child". This actually means "this object cannot be serialized".
Class Instances
Class instances contain methods and internal state that cannot be serialized:
// ❌ Error: Class instances cannot be serialized
class MyClass {
constructor(value) {
this.value = value;
}
method() {
return this.value * 2;
}
}
export default function ServerComponent() {
const instance = new MyClass(42);
returnSymbols
Symbols are unique references that cannot be transmitted over the network:
// ❌ Error: Symbols cannot be serialized
export default function ServerComponent() {
const MY_KEY = Symbol("unique");
returnDate, Map, Set Objects
Built-in objects like Date, Map, and Set cannot be serialized directly:
// ❌ Error: Date objects cannot be serialized
export default function ServerComponent() {
const now = new Date();
returnReact Elements and Components
You cannot pass JSX elements or component references from Server to Client:
// ❌ Error: React elements cannot be serialized
export default function ServerComponent() {
const element = ;
returnCommon Error Messages
"Objects are not valid as a React child"
This confusing error actually means "I tried to serialize this object and failed":
// Causes: "Objects are not valid as a React child"
export default function ServerComponent() {
const data = { onClick: () => {} }; // Function
return ;
}"Only plain objects can be passed to Client Components"
This appears when you try to pass class instances or complex objects:
// Causes: "Only plain objects can be passed"
export default function ServerComponent() {
const data = new FormData();
return"Functions cannot be serialized"
Direct error when passing functions as props:
// Causes: "Functions cannot be serialized"
export default function ServerComponent() {
returnError Boundaries and Serialization
When a Server Component throws an error during rendering, React serializes the error object into the RSC stream:
// Error gets serialized, not thrown
export default async function ServerComponent() {
throw new Error("Something went wrong");
returnThe error is serialized as JSON and transmitted in the RSC stream. During SSR (server-side rendering), this error causes the render to fail. During client-side hydration, the error can be caught by an Error Boundary.
However, Error Boundaries do not work in Server Components. You must wrap Client Components to catch errors:
// ❌ Error Boundary in Server Component - doesn't work
export default function ServerComponent() {
return (
);
}
// ✅ Error Boundary in Client Component - works
export default function ServerComponent() {
returnStreaming and Partial Serialization
React Server Components support streaming, which means parts of the tree can serialize and render while other parts are still computing:
// Streaming allows partial serialization
export default async function ServerComponent() {
const fastData = await db.fastQuery(); // 10ms
const slowData = await db.slowQuery(); // 1000ms
return (
);
}However, if slowData contains non-serializable values, the entire stream fails:
// ❌ Entire stream fails if any part can't serialize
export default async function ServerComponent() {
const fastData = await db.fastQuery(); // Serializable
const slowData = { onClick: () => {} }; // Not serializable
return (
);
}Suspense boundaries help isolate serialization failures:
// ✅ Suspense isolates the error
export default async function ServerComponent() {
return (
);
}Server Actions: The Exception
Server Actions are the only function type that can be serialized. They use special serialization that Next.js can reconstruct on the client:
// ✅ Server Actions serialize correctly
"use server"
export async function createUser(formData: FormData) {
const name = formData.get("name");
await db.user.create({ name });
return { success: true };
}
// Server Component can pass this as a prop
export default function ServerComponent() {
returnRegular async functions (even with "use server" directive) that are not Server Actions cannot be serialized:
// ❌ Not a Server Action - won't serialize
"use server"
export async function helper() {
return await db.query();
}
export default function ServerComponent() {
returnDebugging Serialization Issues
Use TypeScript to Catch Issues Early
Define explicit types for props crossing the boundary:
interface ClientProps {
data: {
id: string;
name: string;
createdAt: string; // string, not Date
};
onSubmit?: ServerAction; // Only Server Actions allowed
}
"use client"
export default function ClientComponent({ data, onSubmit }: ClientProps) {
// TypeScript will warn if you try non-serializable types
}Console.log Before Serialization
Log data before passing to Client Component to verify structure:
export default async function ServerComponent() {
const data = await fetchData();
console.log("Data to serialize:", data); // Inspect before passing
returnTest with Non-Streaming SSR First
Disable streaming to see serialization errors immediately:
// next.config.js
module.exports = {
experimental: {
// Disable streaming for debugging
serverComponentsExternalPackages: []
}
}Performance Implications
Serialization has overhead. Large objects take time to serialize and deserialize:
- Deeply nested objects - O(n) serialization cost
- Large arrays - Each element must be serialized
- Redundant data - Don't pass more than needed
// ❌ Passing too much data
export default async function ServerComponent() {
const posts = await db.post.findMany({ take: 1000 });
returnUse pagination or infinite scroll instead of sending large datasets. Serialize on the server, hydrate incrementally on the client.
The Bottom Line
RSC serialization is a strict boundary that forces you to be intentional about what crosses from server to client:
- Only JSON-serializable data - Primitives, plain objects, arrays
- Convert dates to strings - Use
toISOString()orgetTime() - Extract data from classes - Don't pass instances
- Use Server Actions for functions - Regular functions won't serialize
- Error Boundaries only work client-side - Wrap Client Components, not Server Components
Embrace the constraint. It encourages better architecture by preventing tight coupling between server and client. If you can't serialize something, ask yourself: should this actually live on the client?
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Next.js Server and Client Components
Official Next.js docs on composition patterns and the server-client boundary
React Server Components Specification
RFC document detailing React Server Components architecture and serialization
React Error Boundaries
React docs on error boundaries and how they work in different environments
Tools & Utilities
Further Reading
Error Rendering with RSC
Deep dive into how errors flow through RSC, SSR, and browser rendering environments
Working with Functions and Props Between Server and Client Components
Practical guide to passing data and functions across the RSC boundary
Props Must Be Serializable for Client Components
GitHub discussion on why props must be serializable and common pitfalls
Related Insights
Explore related edge cases and patterns
Advertisement