Skip to content

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.

Related questions

Why can a Server Action be passed but an onClick cannot?
Because a Server Action is not really passed. It is compiled to a reference - an identifier the client uses to call back to the server - which does serialise. An arbitrary function has no such identity; there is nothing to send.
Is adding 'use client' to the page really that bad?
It works, and it is why this error is usually resolved badly. Everything the page imports becomes part of the client bundle, including things that were only ever meant to run on the server. One line moves the boundary to the top of the tree instead of the bottom.