DEV Community

Cover image for Fix: Event handlers cannot be passed to Client props
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Fix: Event handlers cannot be passed to Client props

Event handlers cannot be passed to Client Component props. <button onClick={...} /> If click is a function, make sure it's decorated with "use client" or only imported from a Client Component.is a compile-time boundary check, not a runtime bug: React Server Components render on the server and stream **serialized** output to the client, and a JavaScript function is not serializable — there is no wire format for a closure. The fix is always about which file gets"use client", never about theonClick` syntax itself.

Why this only broke with the App Router

In the Pages Router, every component ships to the browser as JavaScript, so a
function prop is just a normal reference. The App Router renders Server
Components on the server by default and sends the client only a description
of the resulting HTML plus references to Client Component chunks. A function
defined in a Server Component has no client-side identity to reference — the
build catches this and fails instead of shipping a component that silently
does nothing on click.

Shape 1: the handler is defined in the Server Component itself

`tsx
// app/page.tsx — a Server Component (no "use client")
export default function Page() {
function handleClick() {
console.log('clicked');
}

// ❌ handleClick is a server-only function, cannot cross the boundary
return Click me;
}
`

The fix is to push the interactive piece into its own Client Component and
render that from the Server Component — not to sprinkle "use client" at
the top of page.tsx, which would turn the whole page (data fetching
included) into a Client Component.

`tsx
// app/ClickButton.tsx
'use client';

export function ClickButton() {
function handleClick() {
console.log('clicked');
}
return Click me;
}
`

`tsx
// app/page.tsx — stays a Server Component
import { ClickButton } from './ClickButton';

export default function Page() {
return ;
}
`

Shape 2: passing a Server Component's handler down as a prop

`tsx
// ❌ Server Component tries to hand a function to a Client child
import { Counter } from './Counter'; // 'use client'

export default function Page() {
function onIncrement() {
console.log('incremented');
}
return ;
}
`

Even though Counter is a Client Component, onIncrement is still defined in
a Server Component — the function has to be created inside the client
boundary. If the counter's logic genuinely needs server data, fetch that data
in the Server Component and pass it as a plain serializable value (a number,
string, or object), then let the Client Component own its own event handler:

`tsx
// app/Counter.tsx
'use client';
import { useState } from 'react';

export function Counter({ initial }: { initial: number }) {
const [count, setCount] = useState(initial);
return setCount((c) => c + 1)}>{count};
}
`

`tsx
// app/page.tsx
import { Counter } from './Counter';

export default async function Page() {
const initial = await getInitialCount(); // server-only data fetch
return ; // a number, not a function
}
`

Shape 3: it needs to call a Server Action

This is the case that looks like it should need a function prop but does not.
Server Actions are the one exception the framework serializes specially — a
function marked "use server" compiles down to a reference the client can
invoke over the network, so it is passed exactly like a normal prop:

`tsx
// app/actions.ts
'use server';

export async function likePost(postId: string) {
await db.posts.update({ where: { id: postId }, data: { likes: { increment: 1 } } });
}
`

`tsx
// app/page.tsx — Server Component
import { likePost } from './actions';
import { LikeButton } from './LikeButton';

export default function Page({ postId }: { postId: string }) {
return ;
}
`

`tsx
// app/LikeButton.tsx
'use client';

export function LikeButton({ onLike }: { onLike: (id: string) => Promise }) {
return onLike('123')}>Like;
}
`

If the build still throws on this pattern, the action file is missing the
top-level "use server" directive, or the function was defined inline inside
a Server Component instead of imported from a dedicated "use server" module.

Verifying the fix

  1. grep -rn "onClick=\|onChange=\|onSubmit=" app and check every file lacking "use client" at the top — those are the candidates.
  2. Run next build; this error is caught at build time, so a clean build is a real signal here, unlike hydration bugs that only show up at runtime.
  3. Confirm the interactive island is as small as possible — a whole page marked "use client" to fix one button loses server rendering for everything else on it.

Related Incidents


Originally published at https://www.iloveblogs.blog

Top comments (0)