Props that travel over a wire
There is one more rule of the server/client split, and it bites real teams weekly. When a server component renders a client component, the props it passes do not stay inside one program — the server serializes them (writes them down as data), sends them to the browser as part of the page payload, and React rebuilds them there. So props crossing the server → client boundary must be serializable: strings, numbers, booleans, null, and plain arrays and objects made of those.
What cannot be written down and rebuilt: functions, class instances, database handles. Pass a function from a server component to a client component and Next.js stops you with the error every Next.js developer eventually meets:
Error: Functions cannot be passed directly to Client Components
Without this rule the framework would have to ship arbitrary server code to the browser — exactly what server components exist to prevent.
Worked example: the broken delete button
A server component tries to hand its client child a callback:
// app/posts/page.js (server) import DeleteButton from "./delete-button"; // "use client" export default async function Posts() { const posts = await getPosts(); return posts.map((post) => ( <DeleteButton key={post.id} onDelete={() => db.posts.remove(post.id)} // ✗ function prop — build error /> )); }
The fix follows from the rule: send data across the boundary and keep behavior on the side where it runs.
// server: pass the id (a string — serializable)
<DeleteButton key={post.id} postId={post.id} />Inside DeleteButton, the click handler is defined locally and calls the server through a proper channel — a server action or an API route (both are Unit 6). One deliberate exception exists: a server action may be passed as a prop, because Next.js sends a secure reference to it, not the function's code.
Quiz
Which of these props can a server component legally pass to a "use client" component?
Code exercise · javascript
Build the checker Next.js effectively runs on your props (pure JavaScript, runnable here). Write canCrossBoundary(value): return true if the value could be serialized and rebuilt in the browser — strings, numbers, booleans, null, and arrays or plain objects whose contents all pass the same test. Return false for functions (and anything containing one).
Problem
A server component passes formatDate={(d) => d.toLocaleDateString()} to a "use client" component and the build fails. Following the data-across, behavior-local rule, should the fix move the formatting function into the client component, or serialize the function as a string? (answer: 'move' or 'serialize')