'use client' Directive: Common Pitfalls
The 'use client' directive marks a file as a client component, indicating that its code and all its imports run in the browser. Misplacing or overusing this directive is the most common mistake in Server Components applications. A single misplaced 'use client' at the top of a layout can disable server rendering for your entire app. This article covers the seven most dangerous pitfalls and how to avoid them.
Pitfall 1: Over-Using 'use client' in Layouts
Layouts should be server components by default. Adding 'use client' to a layout disables server rendering for the entire subtree. All child components become client components, even if they don't need to be.
// ❌ Disables server rendering for entire app
'use client';
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
);
}
This mistake is surprisingly common. Developers add 'use client' to a layout thinking they need it for styling or analytics, not realizing they've disabled all server-side benefits for the entire application.
// ✅ Keep layout as server component
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
);
}
If you need client-side functionality in the layout, extract it into a separate client component:
// ✅ Keep layout server; wrap with client provider
import { ClientAnalytics } from './client-analytics';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ClientAnalytics />
{children}
</body>
</html>
);
}
// components/client-analytics.tsx
'use client';
import { useEffect } from 'react';
export function ClientAnalytics() {
useEffect(() => {
// Client-side analytics initialization
}, []);
return null;
}
Pitfall 2: Putting 'use client' Too High in the Tree
Every 'use client' directive applies to the entire file and all its imports. Placing it too high in the component hierarchy marks too many components as client components.
// ❌ Marks all utilities as client code
'use client';
import { Button } from './button';
import { Card } from './card';
import { formatDate } from './utils';
export function Dashboard() {
return <Card title="Dashboard"><Button>Action</Button></Card>;
}
Even pure utility functions like formatDate become client code, wasting bundle space.
// ✅ Keep 'use client' at the leaf level
// components/dashboard.tsx
'use client';
import { Button } from './button';
import { Card } from './card';
import { formatDate } from './utils'; // Still runs on client due to import
export function Dashboard() {
return <Card title="Dashboard"><Button>Action</Button></Card>;
}
// lib/utils.ts — no 'use client' directive
export function formatDate(date: Date) {
return date.toLocaleDateString();
}
The best practice: mark only the leaf components that actually need browser APIs as 'use client'. Utility functions, types, and data should live in separate files without the directive.
Pitfall 3: Passing Non-Serializable Data to Client Components
Client components receive props from server components. Those props must be serializable to JSON. Passing functions, class instances, or other non-serializable values causes runtime errors.
// ❌ Attempting to pass a function to a client component
async function ServerComponent() {
const handleClick = () => console.log('clicked');
return <ClientComponent onClick={handleClick} />;
// Error: functions are not serializable
}
// components/client-component.tsx
'use client';
export function ClientComponent({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
// ✅ Use a server action for callbacks
// app/actions.ts
'use server';
export async function handleClick() {
console.log('clicked on server');
// Access database, secrets, etc.
}
// Components
async function ServerComponent() {
return <ClientComponent />;
}
'use client';
import { handleClick } from '@/app/actions';
export function ClientComponent() {
return <button onClick={() => handleClick()}>Click me</button>;
}
Server actions are async functions marked with 'use server' that can be called from client components. They execute on the server and handle mutations or side effects safely.
Pitfall 4: Accidentally Importing Server-Only Code in a Client Component
If a client component imports a module marked as server-only (with 'use server' or containing database code), Next.js will error.
// lib/db.ts — server-only database code
import { db } from 'some-orm';
export async function getUser(id: number) {
return db.users.findById(id);
}
// ❌ Error: client component cannot import server code
'use client';
import { getUser } from '@/lib/db'; // Error!
export function UserProfile({ userId }) {
const user = getUser(userId); // Error!
return <div>{user.name}</div>;
}
Next.js will throw an error like: "Cannot use the 'getUser' function in a client component. Use 'use server' or separate this function into a new file marked with 'use server'."
// ✅ Extract server code into a separate file
// lib/db.ts — server-only
export async function getUser(id: number) {
return db.users.findById(id);
}
// app/actions.ts — server action
'use server';
import { getUser } from '@/lib/db';
export async function fetchUser(id: number) {
return getUser(id);
}
// components/user-profile.tsx — client component
'use client';
import { fetchUser } from '@/app/actions';
export function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // Use with Suspense or useQuery
return <div>{user.name}</div>;
}
Mark server-only modules with a "server-only" comment or use Next.js's server-only package:
// lib/db.ts
import 'server-only';
export async function getUser(id: number) {
// This file can only be imported in server code
}
Importing this file in a client component will throw a build error.
Pitfall 5: Using Browser APIs in Server Components (via Imports)
A server component importing a client component that uses browser APIs will cause runtime errors on the server. Be explicit about boundaries.
// components/local-storage-cache.tsx
'use client';
export function useLocalStorage(key: string) {
// Uses window.localStorage — client-only
}
// ❌ Error if server component imports the client hook
async function ServerComponent() {
const cache = useLocalStorage('key'); // Error!
return <div>{cache}</div>;
}
// ✅ Wrap the browser API usage in a client component
'use client';
import { useLocalStorage } from '@/lib/hooks';
export function CacheDisplay() {
const cache = useLocalStorage('key');
return <div>{cache}</div>;
}
// Server component renders the client wrapper
async function ServerComponent() {
return <CacheDisplay />;
}
The error message is: "window is not defined" or "localStorage is not defined on the server". Always wrap browser API usage in client components.
Pitfall 6: Misunderstanding Boundaries with Async Components
A server component calling an async function is straightforward. A client component cannot directly await in the component body (hooks-only), and a server component cannot be a child of a client component.
// ❌ Attempting async in a client component
'use client';
export function ClientComponent() {
const data = await fetchData(); // Syntax error!
return <div>{data}</div>;
}
// ✅ Use useEffect for async operations in client components
'use client';
import { useEffect, useState } from 'react';
export function ClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
Or use a library like React Query or SWR that abstracts the fetching logic.
Pitfall 7: Marking Shared Utility Components as 'use client'
A utility component (no hooks, no state, no browser API) that is shared between server and client components should not have 'use client'.
// ❌ Unnecessary 'use client' on a shared utility
'use client';
export function Badge({ label }) {
return <span className="badge">{label}</span>;
}
This component doesn't use any client-specific features. Marking it as 'use client' wastes bundle space.
// ✅ Shared utility with no 'use client' directive
export function Badge({ label }) {
return <span className="badge">{label}</span>;
}
This component can be imported and used in both server and client components without any overhead.
A rule of thumb: only mark a file with 'use client' if it directly imports and uses a client-only feature (hooks, browser APIs, event listeners).
Key Takeaways
- Keep
'use client'out of layouts; place it only on leaf components that actually need it. - Mark server-only modules with the
server-onlypackage to prevent accidental client imports. - Pass serializable data to client components; use server actions for callbacks and mutations.
- Wrap browser API usage in client components; never use
windoworlocalStoragein server code. - Don't mark shared utility components as
'use client'unless they directly use client features. - Understand the boundary: server components can render client components, but not vice versa.
Frequently Asked Questions
If I add 'use client' to a layout, does it affect only that layout's children?
Yes. A 'use client' directive in a layout marks the layout and all its child components as client components. All routes under that layout become client-rendered, losing server-side benefits.
Can I use 'use client' in a utility file?
Avoid it. If a utility is pure (no state, no hooks), it doesn't need 'use client'. If it uses browser APIs, consider wrapping it in a client component that exports a hook.
What's the difference between 'use server' and 'use client'?
'use server' marks a function or file as server-only; it runs only on the server and can be called from client components. 'use client' marks a component or module as client-only; it runs only in the browser.
Can a server action call another server action?
Yes. Server actions can call other server actions. Since both run on the server, you can share database access and secrets freely.
How do I know if a component should be 'use client'?
Ask: Does this component use useState, useEffect, other hooks, or browser APIs? If yes, mark it 'use client'. If no, leave it as a server component.