Skip to content

Serializable Props — What Can Cross the Server-Client Boundary

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Serializable Props. We cover key concepts, practical examples, and best practices to help you master this topic.

Props passed from Server Components to Client Components must be serializable — plain data types that can be converted to JSON and back without losing information.

What You'll Learn

You will understand what types are serializable, what types cause errors, how to handle dates and special types, and patterns for preparing data for client consumption.

Why It Matters

Passing a function or class instance from a Server to a Client Component throws a runtime error. Understanding Serialization prevents bugs and keeps your data flow predictable.

Real-World Use

DodaZIP's file manager serializes file metadata (name, size, date, permissions) as plain objects before passing them to the Client Component for rendering.

flowchart LR
    A[Server Component] --> B{Data to pass}
    B --> C[Plain Object]
    B --> D[Array]
    B --> E[String/Number/Boolean]
    B --> F[Date]
    B --> X[Function]
    B --> Y[Class Instance]
    B --> Z[Symbol]
    C --> G[Client Component]
    D --> G
    E --> G
    F --> H[Serialized as ISO string]
    X --> I[RUNTIME ERROR]
    Y --> I
    Z --> I
    style B fill:#1e293b,color:#fff
    style I fill:#dc2626,color:#fff
    style G fill:#0f172a,color:#fff

Serializable Types

These types are safe to pass from Server to Client Components.

// Server Component passing serializable props
async function Dashboard() {
  const user = await db.users.findById(id);
  const recentOrders = await db.orders.findRecent(id);

  return (
    <ClientDashboard
      userName={user.name}                        // string
      age={user.age}                              // number
      isActive={user.status === 'active'}         // boolean
      tags={user.tags}                            // array
      metadata={{                                 // plain object
        lastLogin: user.lastLogin.toISOString(),  // date as string
        role: user.role
      }}
      preferences={null}                          // null
      id={undefined}                              // undefined
    />
  );
}

Expected output: All props pass safely from the Server Component to the Client Component. The date is converted to an ISO string before passing.

Non-Serializable Types That Cause Errors

These types throw a runtime error when passed across the boundary.

async function Page() {
  function handleClick() {                        // Function
    console.log('clicked');
  }

  class UserModel {                               // Class instance
    constructor(data) { this.data = data; }
  }
  const userInstance = new UserModel({ name: 'Alice' });

  const mySymbol = Symbol('unique');              // Symbol

  return (
    <ClientComponent
      onClick={handleClick}                       // ERROR
      userModel={userInstance}                    // ERROR
      symbol={mySymbol}                           // ERROR
    />
  );
}

Expected output: Each of these props causes a runtime error: "Only plain objects, arrays, strings, numbers, booleans, null, and undefined are allowed as props from Server to Client Components. Functions, class instances, and symbols are not serializable."

Handling Dates and Complex Objects

Convert non-serializable types to their serializable equivalents before passing.

// BAD: Passing Date object directly
async function EventPage() {
  const event = await db.events.findById(id);
  return <EventCard
    startDate={event.startDate}  // Date object  not serializable
    endDate={event.endDate}      // Date object  not serializable
  />;
}

// GOOD: Convert to ISO strings
async function EventPage() {
  const event = await db.events.findById(id);
  return <EventCard
    startDate={event.startDate.toISOString()}
    endDate={event.endDate.toISOString()}
  />;
}

// Client Component receives ISO strings
'use client';
function EventCard({ startDate, endDate }) {
  const start = new Date(startDate); // Parse locally
  const end = new Date(endDate);
  return (
    <div>
      <p>Start: {start.toLocaleDateString()}</p>
      <p>End: {end.toLocaleDateString()}</p>
    </div>
  );
}

Expected output: The EventCard receives date strings and converts them to Date objects client-side for formatting.

Preparing Data for Client Components

Transform complex data into serializable shapes before passing.

async function UsersPage() {
  const users = await db.users.findAll();

  // Transform data for client consumption
  const serializableUsers = users.map(user => ({
    id: user.id.toString(),      // Convert ObjectId to string
    name: user.name,
    email: user.email,
    role: user.role,
    createdAt: user.createdAt.toISOString(),
    permissions: user.permissions.map(p => ({
      resource: p.resource,
      action: p.action
    })),
    isAdmin: user.role === 'admin'
  }));

  return <UserTable users={serializableUsers} />;
}

Expected output: The user data is fully serializable. ObjectId fields are converted to strings. Nested permission objects are plain objects. Boolean flags are computed server-side.

Passing Callbacks via Server Actions

Instead of passing functions as props, use Server Actions for server-side callbacks.

// Instead of passing a function prop, use a Server Action
'use server';
export async function deleteUser(userId) {
  await db.users.delete(userId);
  revalidatePath('/users');
  return { success: true };
}

// Client Component calls the Server Action
'use client';
function UserRow({ user }) {
  const [pending, setPending] = useState(false);

  async function handleDelete() {
    setPending(true);
    await deleteUser(user.id);
    setPending(false);
  }

  return (
    <tr>
      <td>{user.name}</td>
      <td>{user.email}</td>
      <td>
        <button onClick={handleDelete} disabled={pending}>
          {pending ? 'Deleting...' : 'Delete'}
        </button>
      </td>
    </tr>
  );
}

Expected output: The deleteUser Server Action handles the server-side logic. The Client Component calls it without needing a function prop to cross the boundary.

Common Mistakes

  1. Passing Date objects directly: Date objects are not serializable. Convert them to ISO strings or timestamps before passing to Client Components.

  2. Passing Mongoose or Prisma model instances: Database models contain non-serializable methods and circular references. Convert them to plain objects with .toJSON() or .lean().

  3. Trying to pass event handlers as props: Functions cannot cross the boundary. Use Server Actions or client-side event handlers defined in the Client Component.

  4. Passing large objects unnecessarily: Large serialized objects increase the HTML size. Pass only the data the Client Component needs.

  5. Forgetting that Map and Set are not serializable: Convert Map and Set to plain objects or arrays before passing them across the boundary.

Practice Questions

  1. What types can be passed as props from Server to Client Components?

Strings, numbers, booleans, null, undefined, plain objects, arrays, and Date objects converted to strings.

  1. Why can't functions cross the server-client boundary?

Functions cannot be serialized to JSON. They contain closure scope and executable code that cannot be transferred as data.

  1. How do you pass a date to a Client Component?

Convert it to an ISO string using toISOString() on the server, then parse it back to a Date object on the client.

  1. What happens if you pass a non-serializable value?

React throws a runtime error during rendering. The error message specifies which prop caused the issue.

  1. How do you handle MongoDB ObjectId across the boundary?

Convert ObjectId to a string using .toString() before passing it as a prop.

Challenge

Create a function that recursively transforms a complex database result (with nested objects, dates, and ObjectIds) into a fully serializable object safe for passing to Client Components.

Frequently Asked Questions

Are React elements serializable?

No. React elements (JSX) cannot be passed as props from Server to Client Components. Use the children prop pattern instead.

Can I pass a Promise as a prop?

No. Promises are not serializable. Resolve them on the server and pass the resolved value.

Are BigInt values serializable?

No. BigInt is not supported in JSON. Convert to string or number before passing.

How do I handle circular references in objects?

Remove or replace circular references before passing. Use libraries like fast-json-stable-stringify to detect circular structures.

Can I pass Error objects as props?

No. Error objects have non-serializable properties. Pass error message strings and error code numbers instead.

Mini Project

Build a Server Component that queries a database with complex nested data, transforms it into a serializable shape, and passes it to a Client Component that renders an interactive data table with sorting.

What's Next

Learn about Context Server and how to manage shared state patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro