skip to content

Serializable Props Across the Boundary

Props crossing from a server component into a client component have to survive a wire format, so functions and class instances are rejected outright. 'Can you pass a callback down?' is the question that reveals whether you understand the boundary is a network hop.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In React 19, props passed from a Server Component to a Client Component are serialized into the RSC payload before they reach the browser. Which kinds of values survive that trip, and which ones make React throw?

level: juniorimportance: must knowfreq 68%

answer

  1. the boundary is a network hop
  2. data crosses, behaviour does not
  3. plain objects only, no prototypes
  4. Date and Map survive, it is not JSON
  5. functions throw unless 'use server'

basics

~20 s

Primitives, plain objects, arrays, Date, Map, Set, typed arrays, JSX elements, promises and Server Functions can cross. Ordinary functions, class instances and locally created symbols throw, because React must write every prop into a wire payload the browser reads back.

solid answer

~50 s

The boundary behaves like a network hop: React serializes every prop into the RSC payload the browser downloads, so a prop has to be expressible in that format. What crosses is data — strings, numbers, booleans, `bigint`, `null`, `undefined`, plain objects and arrays of serializable values, `Date`, `Map`, `Set`, typed arrays and `ArrayBuffer`, `FormData`, JSX elements, promises, and functions that are Server Functions marked with `'use server'`. What throws is behaviour and identity — ordinary functions and closures, class instances or anything whose prototype is not one of those built-ins, and symbols not registered via `Symbol.for`. The check is recursive, so a function nested three levels inside an object prop fails too. Note the format is richer than JSON: a `Date` arrives as a real `Date` and a `Map` as a `Map`. The working rule is: pass data down, keep behaviour inside the client module.

code

jsx · 15 lines
jsx
// server.jsx — a Server Component
import ClientPanel from './ClientPanel';

export default function Panel() {
  return (
    <ClientPanel
      // crosses: primitives, plain object, Date, Set, JSX
      title="Sales"
      total={42n}
      meta={{ region: 'eu', updatedAt: new Date() }}
      tags={new Set(['live'])}
      footer={<p>generated on the server</p>}
    />
  );
}

go deeper

for a junior

Be able to state the allowed list and the forbidden list without hesitating, and give the reason in one sentence: the props are serialized and sent to the browser. Saying "pass data, not functions" already earns the point.

for a middle

Explain why the format accepts a Date and a Map but rejects a class instance — plain objects have nothing but data, a class instance is defined by a prototype that cannot travel. Note that the check is recursive.

for a senior

Show that you treat the boundary as an API contract: map domain objects to explicit plain shapes on the server, and expect the error at server render time with a prop path you can read. Mention that Server Function arguments obey the same rules.

for a principal

Own the standard for what the boundary is allowed to carry: a defined view-model per boundary, one mapping place per entity, and awareness that whatever crosses is both downloadable by the user and a shape you now have to keep stable.

## Why there is a wire format at all A Server Component runs once, on the server, and never ships its code to the browser. Its output is not HTML — it is a serialized description of the UI, the RSC payload (React's "Flight" format), which the browser downloads and React turns back into elements. In that description a Client Component is not executed; it appears as a placeholder that names the client chunk to load plus **the list of props to hand it**. That is the whole rule in one sentence: props crossing from server to client are written into a stream in one process and read back in another. They are data on a wire, not references in a shared heap. Everything else about serializable props follows from it. ## What may cross React documents the serializable set explicitly: - Primitives: `string`, `number`, `bigint`, `boolean`, `undefined`, `null`, and symbols **registered globally** with `Symbol.for('x')`. - Plain objects — those created from an object initializer or spread — whose properties are themselves serializable. - Iterables of serializable values: arrays, `Map`, `Set`, `TypedArray`, `ArrayBuffer`. - `Date`. - `FormData`. - JSX elements (server or client component elements) — this is what makes passing rendered `children` into a client component work. - Promises. - Functions that are Server Functions (exported from a `'use server'` module, or defined with an inline `'use server'` directive). ```jsx // Server Component export default async function Page() { const rows = await db.query('select id, name, created_at from users'); return ( <UserTable rows={rows.map((r) => ({ id: r.id, name: r.name, createdAt: new Date(r.created_at) }))} tags={new Set(['active', 'beta'])} /> ); } ``` ## What throws - **Ordinary functions**, including arrow functions defined inline in the server component's JSX. A function is code plus a closure over server-side scope; neither can be written to a stream. - **Class instances**, and more generally any object whose prototype is not `Object.prototype` or one of the supported built-ins — an ORM model, a `Response`, a `WeakMap`, your own `class Money`. Objects with a `null` prototype are rejected too. - **Symbols not in the global registry**, e.g. `Symbol('rowId')`, because there is no way to recreate the same symbol on the other side. The error surfaces during the server render, and React names the offending prop path in the message — text along the lines of *only plain objects can be passed to Client Components*, or for a function, that functions cannot be passed unless marked `'use server'`. ## Richer than JSON, poorer than a JS heap Candidates often assume the boundary is `JSON.stringify`. It is not, and both directions of that difference matter. Richer: `undefined` survives as `undefined` rather than being dropped; `bigint` survives; a `Date` arrives as a live `Date` you can call `.getTime()` on; a `Map` arrives as a `Map`. You do not need to hand-encode those types. Poorer: nothing that carries *behaviour* survives. There is no way to send a method, a getter's identity, a live subscription, or an object that is meaningful only because of its class. That is not a limitation React could lift — the client process has no such object to reconstruct. ## Direction matters The rule applies to props flowing **server → client**. Two related cases obey the same serialization set: the arguments a client component passes when it calls a Server Function, and that function's return value, both travel over the network with the same constraints. Props between two client components are ordinary JavaScript — functions, class instances and anything else are fine there, because no boundary is crossed. ## The practical consequence Design the boundary as *data in, behaviour local*. If a client component needs to do something, the function that does it lives in the client module (or is a Server Function it calls). If a server component has a rich domain object, it maps it to a plain shape before passing it. Thinking of the props as an API response, rather than as arguments to a function call, gets almost every case right on the first try.

  • Does the same restriction apply when a Client Component calls a Server Function with arguments?
    Yes. The arguments are serialized and sent to the server, and the return value is serialized coming back, using the same set of allowed types. So you can pass an id, a plain object or `FormData`, but not a callback or a class instance in either direction.
  • A prop is a plain object, but one of its nested properties is a function. Does that pass?
    No. Serialization is recursive, so React walks the whole prop value and fails on the nested function just as it would on a top-level one. The error message points at the property path, which is usually enough to find it.
  • Why can you pass JSX as a prop when a component is itself a function?
    You are not passing the component function — you are passing an element, which is a plain description: a type reference, props and children. For a server-rendered element the server has already rendered it, so what crosses is its output, not the code that produced it.

saying these in an interview costs you the question

  • Thinks props are passed by reference like any parent to child
  • Says the payload is JSON, so Dates arrive as strings
  • Claims wrapping a function in an object gets it across
  • Believes class instances work because they are just objects
  • Applies the restriction to client-to-client props as well

context

open as a page

A React Server Component renders `<SaveButton onSave={() => saveRow(id)} />`, where SaveButton is a Client Component, and the render fails with an error about functions. Why does React reject that prop, and what are the legitimate ways to give that button behaviour?

level: middleimportance: must knowfreq 72%

basics

~20 s

A prop crossing into a Client Component is serialized and sent to the browser, and a function is code plus closure over server scope, so it cannot travel. Define the handler in the client module, or pass a Server Function marked 'use server'.

open as a page

In a React Server Component you fetch a record with an ORM and pass the returned model instance straight into a Client Component as a prop, and React throws that only plain objects can be passed. Why does a Date prop work but the model instance not, and how do you fix it?

level: middleimportance: should knowfreq 48%

basics

~20 s

The RSC payload has built-in encodings for a fixed set of types including Date, but any other object must be a plain object built from an initializer. A class instance is defined by its prototype and methods, which cannot travel, so map it to a plain shape first.

open as a page

In React 19, a Server Component passes an un-awaited promise as a prop to a Client Component instead of awaiting it first. What does React do with that promise, and what does the client side have to get right?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Promises are serializable across the RSC boundary. React sends the prop as a pending reference and streams the resolved value as a later chunk of the same response, so the server does not block. The resolved value must itself be serializable, and rejection needs an error boundary.

open as a page

In a large React codebase using Server Components, everything passed as a prop into a Client Component is serialized into a payload the browser downloads and can read. How do you set the standard for what is allowed to cross that boundary?

level: principalimportance: should knowfreq 28%

basics

~20 s

Treat the server-to-client prop boundary as a published API, not an internal call. Define an explicit view model per boundary, map to it in one auditable place per entity, and review it for three things: what the user can read, how many bytes it costs, and how stable the shape is.

open as a page