
Content creator and developer at UICraft Marketplace, sharing insights and tutorials on modern web development.
Save hours of development time with our premium Next.js templates. Built with Next.js 16, React 19, and Tailwind CSS 4.
Get the latest articles, tutorials, and product updates delivered to your inbox.
If you've worked with Next.js App Router and React Server Components, you've likely encountered this frustrating warning in your development environment:
Props must be serializable for components in the "use client" entry file, "onClick" is invalid.
This warning can be particularly confusing when you're passing props between components that you believe are both client components. The error suggests your props need to be serializable—meaning they can be converted to a format suitable for network transmission—but you're not crossing any server boundary... or are you?
In this guide, we'll break down exactly why this warning appears, what causes it, and how to fix it by correctly understanding and implementing the 'use client' directive in your Next.js applications.
Let's look at a common scenario that triggers this warning. Consider this parent-child component relationship:
// components/MessagesContainer.tsx
'use client';
import { useState } from 'react';
import MessageInput from './MessageInput';
export default function MessagesContainer({ initialMessages
// components/MessageInput.tsx
'use client';
type Props = {
messages: Message[];
setMessages: (messages: Message[]) => void; // ⚠️ Function prop
};
export default function MessageInput({ messages, setMessages }: Props) {
const [input, setInput] = useState('');
return (
<input
onKeyDown={(e) => {
if (e.key ===
Both components have 'use client' at the top. Both are clearly meant to run on the client. Yet Next.js (or more specifically, the Next.js TypeScript plugin) warns you that setMessages is not serializable.
This affects not just function props, but any non-serializable data types:
The warning is particularly annoying because the code actually works. Your app runs fine, the functionality is there, but you're left with a persistent warning that clutters your development experience.
Here's the key insight that many developers miss: the 'use client' directive doesn't just mark a component as a "client component." It marks the file as a boundary between server and client components.
As Shu Ding, a Next.js maintainer, explained in GitHub Discussion #46795:
"A 'use client' directive means that component is a boundary between server and client components. Since parent.tsx is already inside the client boundary, everything imported will be client components already."
This is the crucial mental model shift: when you add 'use client' to a file, you're declaring that this file serves as an entry point from the server side to the client side. Any component imported into a file that already has 'use client' is already within the client boundary—it doesn't need (and shouldn't have) its own 'use client' directive.
The official Next.js documentation makes this clear:
"You do not need to add the 'use client' directive to every file that contains Client Components. You only need to add it to the files whose components you want to render directly within Server Components."
So why does the warning appear? Because when you mark MessageInput.tsx with 'use client', you're telling Next.js: "This component might be imported directly from a Server Component." And if that were to happen, the props being passed would need to cross the server-client network boundary. Since functions cannot be serialized and sent over the network, the warning fires.
The TypeScript plugin is essentially warning you: "If you import this component from a Server Component, you won't be able to pass function props."
The solution is straightforward: remove 'use client' from components that are only used by other client components.
Here's the corrected version of our example:
// components/MessagesContainer.tsx
'use client'; // ✅ This IS a boundary - used in Server Components
import { useState } from 'react';
import MessageInput from './MessageInput';
export default function MessagesContainer({ initialMessages }) {
const [messages, setMessages] = useState(initialMessages);
return (
<div>
<MessageInput
messages={messages}
setMessages={setMessages} // ✅ No warning!
/>
</div
// components/MessageInput.tsx
// ✅ NO 'use client' - this is imported by a client component
type Props = {
messages: Message[];
setMessages: (messages: Message[]) => void;
};
export default function MessageInput({ messages, setMessages }: Props) {
const [input, setInput] = useState('');
return (
<input
onKeyDown={(e) => {
if (e.key === 'Enter'
By removing 'use client' from MessageInput.tsx, we're no longer declaring it as a server-client boundary. It's simply a component that will be used by other client components. Since MessagesContainer.tsx has 'use client', anything it imports (including MessageInput) is automatically treated as client-side code.
The warning disappears because there's no boundary to cross.
Sometimes you genuinely need a component that can be used from both contexts. In this case, you have two options:
Option 1: Create a wrapper component
// components/Button.tsx
// Base button - no 'use client', can receive any props internally
export function ButtonBase({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
// components/ClientButton.tsx
'use client';
import { ButtonBase } from './Button';
// This wrapper is the boundary
export function ClientButton({ onClick, children }) {
return <ButtonBase onClick={onClick}>{children}
Option 2: Accept only serializable props at the boundary
If your component must be a boundary and accept function props, consider whether the function can be a Server Action:
// components/SubmitButton.tsx
'use client';
type Props = {
submitAction: () => Promise<void>; // ✅ Server Actions ARE serializable
label: string;
};
export function SubmitButton({ submitAction, label }: Props) {
return (
<form action={submitAction}>
<button type="submit">{label}</button>
</form>
);
}Server Actions are the one exception to the "functions aren't serializable" rule. When defined with 'use server', they create a special reference that can be passed across the boundary.
To avoid the props must be serializable issues entirely, adopt these architectural patterns:
Before adding 'use client', ask yourself:
If the answer is "only used by client components," don't add the directive.
Keep your client boundary at the highest necessary level. This reduces the number of places where serialization matters:
// app/dashboard/page.tsx (Server Component)
import { DashboardClient } from './DashboardClient';
export default async function DashboardPage() {
const data = await fetchDashboardData(); // Server-side data fetching
return <DashboardClient initialData={data} />; // Serializable data passed
}// app/dashboard/DashboardClient.tsx
'use client';
import { useState } from 'react';
import { Chart } from './Chart';
import { DataTable } from './DataTable';
import { Filters } from './Filters';
// Single boundary - all child components are automatically client-side
export function DashboardClient({ initialData }) {
const [filters, setFilters] = useState({});
return (
<div>
<Filters onFilterChange={setFilters
None of Chart, DataTable, or Filters need 'use client' because they're all imported within the client boundary.
Design your data flow so that non-serializable values (functions, state, refs) stay entirely on one side of the boundary:
// ❌ Bad: Passing callback across boundary
<ClientComponent onSubmit={handleSubmit} />
// ✅ Good: Handle form with Server Action
<ClientComponent submitAction={serverAction} />
// ✅ Good: Keep callback within client boundary
'use client';
function ClientComponent() {
const handleSubmit = () => { /* client-side logic */ };
return <ChildComponent onSubmit={handleSubmit} />;
}server-only Package for Extra SafetyFor modules that should never accidentally end up in the client bundle, use the server-only package:
// lib/database.ts
import 'server-only';
export async function getUsers() {
return await db.users.findMany();
}If you accidentally import this into a Client Component, you'll get a build-time error rather than a runtime surprise.
According to the React documentation, these types can safely cross the boundary:
'use server'When designing your component interfaces, ensure props that cross boundaries stick to these types.
The 'use client' directive marks a file as a boundary, not just as "client code." Only files that are directly imported from Server Components need this directive.
Components imported by client components don't need 'use client'. They're automatically part of the client bundle since they're within the boundary.
The serialization warning is a safety check for potential boundary crossings, even if your current usage doesn't cross a boundary.
Server Actions are the exception—they are serializable and can be passed as props across the boundary.
Design your component tree thoughtfully—push client boundaries as high as reasonable and keep non-serializable data within its context.
Audit your codebase: Find all files with 'use client' and ask if each truly needs to be a boundary. Remove the directive from components that are only used by other client components.
Read the GitHub discussions: Issue #74343 and Discussion #46795 contain valuable maintainer insights and community discussion about this topic.
Review the official docs: The Next.js documentation on Server and Client Components and the use client directive reference are essential reading for mastering this pattern.
By understanding that 'use client' defines a boundary rather than simply marking "client code," you'll write cleaner Next.js applications with fewer warnings and a better mental model of how React Server Components actually work.
Adopt TypeScript strict mode: This helps catch serialization issues at compile time rather than runtime, making the development experience smoother.
Consider component architecture early: When starting new features, sketch out which components need interactivity and identify where your single boundary should be. This prevents the "add 'use client' everywhere" pattern from taking hold.