React Server Components: the client/server boundary
- Last updated
- Prerequisites:
- React component model
- Next.js App Router basics
- react
- server-components
- nextjs
- serialization
- rsc
- architecture
Read at your depth
The practical view
In Next.js App Router, components are Server Components by default: export default async function Page() { const data = await fetch(...); return <List items={data} />; } runs entirely on the server — the async/await, the fetch, the file system access — and ships only rendered output. Files with 'use client' at the top become Client Components: their JS bundle ships to the browser and they can use useState, useEffect, and event handlers. The boundary rule: a Client Component can be rendered inside a Server Component, but props crossing the boundary must be serializable (no functions, no class instances, no Date unless wrapped in a serializable form).
The same idea in other frameworks
// page.jsx (Server Component, no 'use client')
export default async function Page() {
const posts = await db.query('SELECT * FROM posts');
return <PostList posts={posts} />;
}// Angular renders on the server via Angular Universal/SSR;
// @defer marks lazy client hydration blocks instead.// Nuxt: composables auto-imported; useAsyncData runs on server
// during SSR and hydrates the client from the payload.// SvelteKit: +page.server.ts load runs on the server;
// the page component hydrates with the returned data.Legacy vs modern
Fetching data in the client component vs in a server component
Client-side fetching ships the fetch logic, the endpoint URL, and double-loads state; a server component fetches once, server-side, and passes serializable props down.
'use client';
const [posts, setPosts] = useState([]);
useEffect(() => { fetch('/api/posts').then(r => r.json()).then(setPosts); }, []);export default async function Posts() {
const posts = await db.query('SELECT * FROM posts');
return posts.map((p) => <li key={p.id}>{p.title}</li>);
}Interview gotchas
Context
This question exposes whether you truly understand serialization rather than having memorized the 'use client' rule.
The mechanical answer
The RSC payload crosses the network as a serialized React Flight stream. Functions are not serializable — they close over server-side scope (the request, the database handle, secrets) and there is no standard wire format that could preserve their semantics safely. If React silently shipped functions, it would either leak server scope or break the boundary entirely. The only functions that exist on the client are those defined in client modules (event handlers inside 'use client' files), which the bundler places in the client chunk. Passing a function from a server module to a client component would require the server module to be in the client bundle, which is exactly what the boundary prevents.
Trap
The naive answer is 'functions can't be JSON serialized' — technically true but incomplete. The deeper point is that a server function's closure references the server runtime; even if you could stringify it, executing it client-side would fail or leak. Interviewers also appreciate when you mention server actions as the sanctioned way to pass a callable across the boundary — they are RPC stubs, not closures.
Context
A classic 'explain RSC to a senior engineer' question that separates knowledge of the protocol from knowledge of the mental model.
The mechanical answer
During the server render, when React reaches a client component, it does not evaluate it — the client component is a module reference. The server writes the element into the Flight stream with the client module's id (a virtual module path resolved by the bundler plugin) plus its serializable props. On the client, React's Flight parser resolves that module reference against the client module registry, imports the actual component, and creates the fiber with the received props. The two sides are bound by a shared module id space that the webpack/turbopack RSC plugin maintains. This is also why client components must be in 'use client' files: the bundler needs a static boundary to know which module graph is server-only.
Trap
A common wrong claim is 'the client re-renders the whole tree'. The client only hydrates client subtrees and reconstructs server-rendered elements from the stream. Another trap: thinking the server renders the client component 'to HTML' during the RSC phase — the Flight stream is not HTML; it is a serialized tree, and the HTML for the initial page is generated separately by the SSR pass.