Fix: Functions cannot be passed directly to Client Components
The Symptom
You are building an app with the Next.js App Router. You created a parent Server Component and tried to pass an onClick handler or an API fetch function down to a child "use client" component. The build immediately crashes, presenting this verbose serialization error in your terminal and browser overlay:
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". Or make sure you don't pass it as a prop.
<ClientButton onClick={function onClick}>
^^^^^^^^^^^^^^^^^
The Cure
This error forces you to decide where the code inside the function should actually execute. You have two distinct solutions depending on your architecture.
Cure 1: The Database Mutation (Server Actions)
If the function needs to securely talk to a database or use a secret API key, convert it into an asynchronous Server Action by injecting "use server" at the top of the function body.
import ClientButton from './ClientButton';
import { db } from '@/lib/db';
export default async function ServerPage() {
// MUST be async to use Server Actions
async function deleteUser(userId: string) {
"use server"; // Tells Next.js to compile this into a hidden API endpoint
await db.user.delete({ where: { id: userId } });
}
return <ClientButton action={deleteUser} userId="123" />;
}
Cure 2: The UI Interaction (Move to Client)
If the function is just toggling UI state (like opening a modal, calculating math, or playing an animation), do NOT pass it from the server. Define the function entirely inside the Client Component.
"use client";
import { useState } from 'react';
export default function ClientButton() {
const [isOpen, setIsOpen] = useState(false);
// Define the function HERE, where it lives in the browser
const toggleModal = () => setIsOpen(!isOpen);
return <button onClick={toggleModal}>Open</button>;
}
Deep Dive: The Network Boundary and Serialization
To truly master Next.js 13+, you must understand the Network Boundary. The Server Component runs on an actual machine in a data center (Node.js). The Client Component runs on the user's laptop or smartphone (The Browser).
When you pass a prop from a Server Component down to a Client Component, that data must travel across the internet. To cross the internet, the data must be serialized into a string (like JSON).
Here is the core issue: You can serialize strings, numbers, booleans, arrays, and plain objects. You cannot serialize a JavaScript function. A function contains scope, closures, and execution context. If the server tries to stringify an onClick = () => console.log('hello') function and send it to the browser, it breaks.
How "use server" Performs Magic
When you apply "use server" to an async function and pass it to a Client Component, Next.js performs a brilliant framework trick behind the scenes.
- It extracts the function code and leaves it securely on the server.
-
It generates a unique, hidden
POSTAPI endpoint. -
It passes a tiny "proxy" function to the Client Component that simply knows how to execute a
fetch()request to that hidden API endpoint.
Because of this proxy generation, the client never actually receives the raw server function, preventing the serialization error while maintaining seamless integration.
Loading logs...