Next.js error
Event Handlers Cannot Be Passed to Client Component Props
The error
Error: Event handlers cannot be passed to Client Component props. <button onClick={function onClick}>. If you need interactivity, consider converting part of this to a Client Component.A function crossed the server-client boundary, and functions do not serialise. The fix is almost never to add 'use client' at the top of the file that threw.
A Server Component renders to a serialised description of UI, which is sent to the browser. Strings, numbers, arrays, plain objects and Server Action references survive that trip. Functions do not - there is no way to send a closure over a network.
So when a Server Component writes onClick={...} on a Client Component, or
passes a callback as a prop, React has nothing to serialise and stops.
The fix that is almost always right
Move the interactivity into its own Client Component, and keep the page a Server Component.
// app/page.tsx - Server Component, no 'use client'
import { CopyButton } from './copy-button';
export default async function Page() {
const token = await getToken(); // server-only, stays server-only
return (
<>
<h1>Your API token</h1>
<code>{token}</code>
<CopyButton value={token} /> {/* a string crosses, not a function */}
</>
);
}// app/copy-button.tsx
'use client';
export function CopyButton({ value }: { value: string }) {
return (
<button onClick={() => navigator.clipboard.writeText(value)}>
Copy
</button>
);
}The handler is defined on the side that runs it. The only thing crossing the boundary is a string.
That is the whole pattern: the data crosses, the behaviour does not.
The three shapes this takes
A handler written inline in a server file. The example above. Extract the element that needs the handler.
A callback prop into a shared component. <Modal onClose={...} /> where
Modal is a client component and the page is a server one. Invert it: let the
modal own its own close state, or lift the whole thing into a client component
that the server page renders as a child.
A function in an object. <Chart config={{ format: (n) => ... }} />. The
error message points at the prop, not at the nested key, which makes this one
slow to find. Pass a named format instead of a function - format="currency" -
and resolve it on the client.
What looks like a fix and is not
Adding 'use client' to the file that threw. The error stops immediately,
which is why this is the most common resolution and the most expensive one.
The directive marks a boundary, not a file: everything imported below it joins
the client bundle. A page that was rendering on the server and shipping almost
no JavaScript now ships its whole import graph - and any server-only code in
that graph, including database clients and secrets, either breaks the build or,
worse, does not.
Where use client actually costs you is the same
decision measured on a real bundle.
Wrapping the handler in useCallback. It is still a function. This changes
nothing except adding a hook, and a hook in a Server Component is its own error.
Passing a Server Action instead, for a UI interaction. Server Actions do cross the boundary, so this compiles. But you have turned a click that should have been instant into a network round trip. Use them for mutations, which is what they are - and the three things people get wrong about them covers where the line sits.
Why the boundary is worth defending
Every component above the client boundary is HTML you do not pay for twice. Every component below it is JavaScript downloaded, parsed and executed on a phone. This error is the framework refusing to let the boundary move by accident - which is more useful than it feels at the moment it appears.
If the boundary in your application has drifted upward over time, that is measurable: First Load JS per route, in the build output, is the number. An audit puts a figure on what pulling it back down would return.
