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?
answer
- props are serialized, closures are not
- code plus captured scope cannot travel
- behaviour lives where it runs
- the 'use server' escape hatch
- reference to an endpoint, not a copy
basics
~20 sA 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'.
solid answer
~50 sThe failure is not a lint rule, it is physics: the server component's output is a payload the browser downloads, and every prop in it has to be written to that stream. An arrow function is compiled code plus a closure over server-side variables, so there is nothing to write. Three legitimate fixes. First and most common: move the behaviour into the client module — `SaveButton` declares its own `onClick` and receives only the data it needs, such as the `id`. Second: pass a **Server Function** — a function marked with `'use server'`. React serializes it as an opaque reference, and calling it from the browser sends a request back to the server to run the real function. Third: pass no function at all and let the client component import the client-side helper it needs. The general shape is: data crosses the boundary, behaviour is defined on the side that runs it.
code
jsx · 10 lines// SaveButton.jsx — behaviour defined on the side that runs it
'use client';
export default function SaveButton({ id, onSave }) {
return (
<button onClick={() => onSave(id)}>
Save
</button>
);
}go deeper
Recall the plain rule: you cannot hand an event handler from a Server Component to a Client Component. Say that the handler should be written inside the client component and given the data it needs, such as an id.
Explain the mechanism — the prop must be written into a payload and a function is code plus closure — and name both routes out: define the handler client-side, or expose a Server Function with 'use server' that serializes as a reference.
Decide between the two routes on where the work must run and what data may be exposed, and treat a Server Function prop as a reachable endpoint: validate its arguments and re-check permissions inside it rather than trusting the caller.
Set the convention for which behaviours are allowed to become server-callable at all, where those functions live, and how the team reviews them — every one you expose as a prop is a piece of public surface area, not an internal helper.
## What the error is actually telling you When a Server Component renders a Client Component, React does not call the client component. It emits a description into the RSC payload: which client chunk to load, and what props to give it. The browser downloads that payload and instantiates the component there. So the props are values that must survive being written out in one process and read back in another. An arrow function fails both halves. Its body is code that lives only in the server bundle — the client bundle never received it. And it closes over server-side scope: `id`, `saveRow`, a database handle, a request-scoped variable. Even if React could ship the source text, the closure it captured has no meaning in the browser. React therefore raises an error at server render time, roughly "functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with 'use server'", and it names the prop. ## Fix 1 — behaviour belongs to the client module This is the right answer most of the time. The client component owns its interactivity; the server component supplies the data it needs to act. ```jsx // SaveButton.jsx 'use client'; import { saveRow } from './client-api'; export default function SaveButton({ id }) { return <button onClick={() => saveRow(id)}>Save</button>; } ``` The server component now renders `<SaveButton id={row.id} />`. Only a number crosses. Notice what changed conceptually: the server stopped trying to inject behaviour and started supplying parameters. ## Fix 2 — a Server Function crosses as a reference Sometimes the behaviour genuinely has to run on the server: it touches the database, uses a secret, or must not be reimplemented in the client bundle. That is what Server Functions are for. A function defined in a module with a top-level `'use server'` directive, or defined with an inline `'use server'` directive inside a Server Component, becomes passable as a prop. ```jsx // actions.js 'use server'; export async function saveRow(id) { await db.rows.update(id, { saved: true }); } ``` ```jsx // Server Component import { saveRow } from './actions'; import SaveButton from './SaveButton'; export default function Row({ id }) { return <SaveButton id={id} onSave={saveRow} />; } ``` What crosses is not the code. React serializes an opaque reference — an identifier the runtime can map back to that function — and the client receives a callable stub. Invoking it from the browser sends a request to the server, which runs the real function and returns its (serializable) result. The mental model to state out loud: a Server Function prop is an RPC endpoint handle, not a copied closure. Two consequences worth naming. The arguments you pass and the value returned obey the same serialization rules as props, so you can pass an id or `FormData` but not a callback. And because it is invokable from the browser, it is a network-reachable entry point — validate its inputs and re-check authorization inside it rather than assuming the caller is your own UI. ## Fix 3 — bind on the server, still a Server Function If a handler needs data the client should not carry, you can produce a specialised Server Function on the server — for example by calling `.bind(null, id)` on an imported Server Function — and pass that. The bound arguments are serialized as part of the reference, so this is still a data-crossing operation, not a closure-crossing one. ## The distinction interviewers are probing Candidates who have only written client React think of props as arguments to a function call, so passing a callback down is reflex. The question exists to see whether you have internalised that this particular parent/child pair sits on two sides of a network hop. Once you say "the boundary is a serialization boundary; behaviour cannot be serialized unless it is exposed as a callable endpoint", every other case — class instances, symbols, promises — follows from the same sentence. ## What does *not* fix it Wrapping the function in an object, in an array, or behind a getter does not help — serialization is recursive. `useCallback` is irrelevant; it is a client hook and the problem is not identity stability. Stringifying the function and `eval`-ing it on the client is not a serious answer and gives away that the model has not landed.
- What exactly does the browser receive when you pass a Server Function as a prop?An opaque reference plus a callable stub. The function body never reaches the client bundle. Calling the stub sends a request to the server with the serialized arguments, the real function runs there, and its serializable return value comes back. Practically, it is a handle to a remote endpoint.
- Would wrapping the callback in an object, like handlers={{ onSave }}, get it past the check?No. Serialization walks the prop recursively, so the nested function fails just the same. The only thing that changes a function from unserializable to serializable is marking it as a Server Function with 'use server'.
- You need the handler to run in the browser but it depends on a value only the server knows. What do you do?Pass the value, not the behaviour — provided the value is safe for the browser to see, since it lands in a payload the user can read. Keep the function in the client module and give it the id or token it needs as a serializable prop.
- Can a Client Component pass a callback down to another Client Component?Yes, freely. The serialization rule only applies where the server→client boundary is crossed. Below a 'use client' entry point everything is ordinary client JavaScript, so functions, class instances and any other value pass by reference as usual.
saying these in an interview costs you the question
- Says useCallback or useMemo would make the function serializable
- Thinks the callback would run on the server automatically
- Believes hiding the function inside an object bypasses the check
- Claims 'use server' ships the function body to the browser
- Treats the error as a bundler bug rather than a boundary rule