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?
answer
- the boundary is a network hop
- data crosses, behaviour does not
- plain objects only, no prototypes
- Date and Map survive, it is not JSON
- functions throw unless 'use server'
basics
~20 sPrimitives, 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 sThe 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// 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
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.
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.
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.
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