Lesson 49 of 81
"use client" doesn't mark one component — it draws a line through the whole module graph underneath it, and the one prop type that can never cross that line in either direction is a plain function.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Layouts and pages are Server Components by default — they run only on the server, can fetch data and touch secrets directly, and send React a compact description of their output (the RSC payload) rather than JavaScript. "use client" at the top of a file draws a boundary: everything that file imports and renders directly joins the client bundle and ships as real JavaScript to the browser, where it hydrates and becomes interactive.
The boundary isn't drawn around one component — it's drawn around a module graph. Marking a file "use client" pulls in every module it imports and every component it renders directly, whether or not you intended all of that to ship to the browser. A Server Component can still be passed into a Client Component as children or another prop — React renders it on the server ahead of time and hands the Client Component its already-rendered output — but anything that actually crosses the boundary as a live reference has to survive being serialized into the RSC payload. Most values do. A plain function does not.
// app/dashboard/page.tsx — Server Component
import { Counter } from "./counter";
export default async function DashboardPage() {
const initialCount = await getInitialCount();
function handleReset() {
console.log("reset requested");
}
return <Counter initialCount={initialCount} onReset={handleReset} />;
}
// app/dashboard/counter.tsx
"use client";
import { useState } from "react";
export function Counter({
initialCount,
onReset,
}: {
initialCount: number;
onReset: () => void;
}) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={onReset}>Reset</button>
</div>
);
}
This fails at build or runtime with some form of "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with 'use server'." handleReset is a plain function defined in a Server Component — there's no way to represent "a live reference to a function running on the server" inside the RSC payload sent to the browser, so React refuses outright. The fix is one of two real options: mark handleReset with "use server" so it becomes an actual Server Action — a callable reference the client can invoke over the network — or, far more often the right call here, just move the logic entirely inside Counter itself, since resetting a piece of client state doesn't need the server at all.
"use client" doesn't mean "this code only runs in the browser." A Client Component still renders once on the server to produce the initial HTML; the directive's real job is marking a module-graph boundary so React knows what to include in the client JavaScript bundle and what to hydrate."use client" file imports, and every component it renders directly, rides along into the client bundle — a large import pulled in "just for one small piece" still ships in full.children) is fine — it's specifically functions and other non-serializable values that aren't. The interleaving pattern (a Server Component passed as children into a Client Component wrapper like a modal) works because React renders the Server Component's output ahead of time; what can't survive the trip is a live function reference or anything else the RSC payload has no format for."use server"-marked function can cross this boundary as something callable. A plain function defined in a Server Component and handed to a Client Component as a prop isn't a special case that sometimes works — it always fails, because there's no mechanism for the client to invoke it unless Next.js has wrapped it as a Server Action.<html> in a provider undoes a lot of what makes the rest of the tree optimizable as static Server Components.This is the same boundary the server-only/client-only package convention exists to guard at the module level, and the same one Server Actions exist to deliberately punch a hole through for mutations — both of those topics assume the mental model this lesson builds: a boundary around a module graph, crossable only by values the RSC payload actually has a shape for.