You have likely encountered this error while refactoring an application to the Next.js App Router or attempting to pass an event handler from a parent Server Component to a child Client Component:
Error: Functions cannot be passed directly to Client Components unless they are explicitly exposed by "use server".
This error acts as a hard stop during development. It highlights a fundamental misunderstanding of the network boundary introduced by React Server Components (RSC).
The Root Cause: The Serialization Boundary
To understand why this fails, you must understand how Next.js renders your application.
- Server-Side Rendering: React renders the Server Components into a special format called the RSC Payload. This is a JSON-like tree representing your UI.
- Network Transmission: This payload is streamed to the browser.
- Hydration/Reconciliation: The browser parses the payload and reconciles it with the DOM and Client Components.
Because the data flows over the network (from Node.js/Edge to the Browser), everything passed as a prop to a Client Component must be serializable.
The serialization protocol supports primitives (strings, numbers), JSON objects, Arrays, Maps, Sets, and Dates. It does not support Functions.
A function in JavaScript is not just text code; it is a closure. It captures the scope in which it was defined. If you define a function inside a Server Component, that function has access to server-side memory, database connections, and environment variables. Serializing that function to the client would require bundling the entire server environment and sending it to the browser—a massive security vulnerability and technical impossibility.
The Fix: Server Actions
To pass executable logic from a Server Component to a Client Component, you cannot pass a standard JavaScript function. You must pass a Server Action.
A Server Action is a function explicitly marked with the 'use server' directive. Next.js treats these differently. Instead of attempting to serialize the function code, Next.js generates a unique reference ID (a URL endpoint). The client receives this ID. When the client invokes the function, it sends a POST request to the server using that ID, and the server executes the code.
Here is the architectural pattern to solve this using TypeScript and modern Next.js practices.
Scenario: A Newsletter Subscription Form
We want a Server Component (the Page) to provide the subscription logic, and a Client Component (the Form) to handle the user interaction and loading states.
1. Define the Server Action (actions.ts)
Create a dedicated file for your actions. This ensures clear boundary separation and ensures the directive is applied correctly at the module level.
// src/app/newsletter/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
// Mock DB delay
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export type SubscribeState = {
message: string;
success: boolean;
};
export async function subscribeToNewsletter(prevState: SubscribeState | null, formData: FormData): Promise<SubscribeState> {
// Simulate network latency
await delay(1000);
const email = formData.get('email');
// Basic validation
if (!email || typeof email !== 'string' || !email.includes('@')) {
return {
message: 'Please enter a valid email address.',
success: false
};
}
try {
// Database logic would go here
console.log(`[SERVER] Subscribing user: ${email}`);
// Revalidate cache if this action changes data visible on the page
revalidatePath('/newsletter');
return {
message: 'You have been successfully subscribed!',
success: true
};
} catch (error) {
return {
message: 'Database error. Please try again.',
success: false
};
}
}
2. Create the Client Component (NewsletterForm.tsx)
This component consumes the action. We use the useActionState hook (available in React 19/Next.js 15, formerly useFormState) to handle the action's return values and pending status.
// src/app/newsletter/NewsletterForm.tsx
'use client';
import { useActionState } from 'react';
import { type SubscribeState, subscribeToNewsletter } from './actions';
// We can also accept the action as a prop if we want to keep this component generic,
// but importing it directly is often cleaner for specific features.
// For this example, we will adhere to the prompt's problem: passing logic via props.
interface NewsletterFormProps {
action: (prevState: SubscribeState | null, formData: FormData) => Promise<SubscribeState>;
}
const initialState: SubscribeState = {
message: '',
success: false,
};
export function NewsletterForm({ action }: NewsletterFormProps) {
const [state, formAction, isPending] = useActionState(action, initialState);
return (
<form action={formAction} className="flex flex-col gap-4 max-w-md border p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-bold">Subscribe for Updates</h3>
<div className="flex flex-col gap-2">
<label htmlFor="email" className="text-sm font-medium text-gray-700">
Email Address
</label>
<input
id="email"
name="email"
type="email"
required
placeholder="engineer@example.com"
className="p-2 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<button
type="submit"
disabled={isPending}
className={`p-2 rounded text-white font-medium transition-colors ${
isPending ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'
}`}
>
{isPending ? 'Subscribing...' : 'Subscribe'}
</button>
{state.message && (
<p
aria-live="polite"
className={`text-sm ${state.success ? 'text-green-600' : 'text-red-600'}`}
>
{state.message}
</p>
)}
</form>
);
}
3. The Server Component (page.tsx)
This is where the error usually occurs. You cannot define an inline closure here (e.g., const handleSubmit = () => ...) and pass it down. You must import the Server Action and pass that.
// src/app/newsletter/page.tsx
import { subscribeToNewsletter } from './actions';
import { NewsletterForm } from './NewsletterForm';
export default function NewsletterPage() {
return (
<main className="min-h-screen flex items-center justify-center bg-gray-50">
{/*
This works because 'subscribeToNewsletter' is marked with 'use server'
in its definition file. Next.js passes a reference, not the function code.
*/}
<NewsletterForm action={subscribeToNewsletter} />
</main>
);
}
Why This Works
When subscribeToNewsletter is passed into NewsletterForm:
- Compilation: Next.js sees the
'use server'directive inactions.ts. - Reference Creation: It creates an RPC-style reference for this function.
- Serialization: When
page.tsxrenders, the prop passed toNewsletterFormis notfunction subscribeToNewsletter() { ... }. It is effectively an object resembling:{ "$$typeof": Symbol.for("react.server.reference"), "$$id": "c7b3d8..." } - Client Execution: The Client Component receives this reference. When
formActionis triggered, React intercepts it, looks up the ID, and sends a POST request to the server to execute the actual function logic securely within the server environment.
Conclusion
The error "Functions cannot be passed directly to Client Components" is a safety mechanism. It prevents the accidental exposure of server-side logic and secrets to the browser.
To resolve it, stop thinking of passing "functions" and start thinking of passing "Server Actions." By moving your logic into a file marked with 'use server' or using the directive at the top of an async function scope, you transform your code into a serializable reference that bridges the network gap securely.