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.

tsx (page.tsx - Server Component)
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.

tsx (ClientButton.tsx)
"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.

Because of this proxy generation, the client never actually receives the raw server function, preventing the serialization error while maintaining seamless integration.

Developer Logs

Loading logs...