Tutorial
December 20, 2024
10 min read
When to Use 'use client' in Next.js
Server vs client components in Next.js 13+. When you need 'use client', when you don't, and what breaks if you get it wrong.
One of the biggest changes in Next.js 13+ is the introduction of React Server Components. This paradigm shift can be confusing for developers migrating from traditional React, but understanding it is crucial for building performant Next.js applications.
The Fundamental Difference
Server Components
- Render on the server
- No JavaScript sent to client
- Can access backend resources
- Default in Next.js 13+
- Cannot use hooks or browser APIs
Client Components
- Render on client
- JavaScript sent to browser
- Can use React hooks
- Need 'use client' directive
- Can handle interactivity
Server Components: The Default
In Next.js 13+, all components are server components by default. They render on the server and send only HTML to the client.
// app/products/page.js
// This is a Server Component (default)
async function getProducts() {
const res = await fetch('https://api.example.com/products');
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
<h1>Products</h1>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>${product.price}</p>
</div>
))}
</div>
);
}
Benefits of Server Components
- Smaller bundle size: No JavaScript sent to client
- Direct backend access: Query databases directly
- Better SEO: Fully rendered HTML
- Faster initial load: No hydration needed
- Secure: API keys stay on server
Client Components: When You Need Interactivity
Use client components when you need interactivity, browser APIs, or React hooks like useState and useEffect.
// components/Counter.js
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
When to Use Client Components
- Using React hooks (
useState, useEffect, etc.)
- Handling user interactions (clicks, form inputs)
- Accessing browser APIs (
window, localStorage)
- Using event listeners
- Using browser-only libraries
- Managing client-side state
Composing Server and Client Components
The real power comes from combining both types. A common pattern is to have server components fetch data and pass it to client components for interactivity.
// app/dashboard/page.js (Server Component)
import ClientChart from '@/components/ClientChart';
async function getData() {
const res = await fetch('https://api.example.com/analytics');
return res.json();
}
export default async function Dashboard() {
const data = await getData();
return (
<div>
<h1>Dashboard</h1>
{/* Server-rendered content */}
<p>Total Users: {data.totalUsers}</p>
{/* Client component for interactivity */}
<ClientChart data={data.chartData} />
</div>
);
}
// components/ClientChart.js (Client Component)
'use client';
import { useState } from 'react';
import { LineChart } from 'recharts';
export default function ClientChart({ data }) {
const [timeRange, setTimeRange] = useState('week');
return (
<div>
<select onChange={(e) => setTimeRange(e.target.value)}>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
</select>
<LineChart data={data} />
</div>
);
}
Common Patterns and Best Practices
Pattern 1: Keep Client Components Small
Push 'use client' as far down the component tree as possible.
// Problem: Bad: Entire page is client component
'use client';
export default function Page() {
const [search, setSearch] = useState('');
return (
<div>
<Header />
<SearchBar value={search} onChange={setSearch} />
<ProductList />
<Footer />
</div>
);
}
// Solution: Good: Only interactive part is client component
export default function Page() {
return (
<div>
<Header />
<SearchBar /> {/* Only this needs 'use client' */}
<ProductList />
<Footer />
</div>
);
}
Pattern 2: Pass Server Data to Client Components
Fetch data in server components and pass it as props to client components.
// app/users/page.js (Server)
async function getUsers() {
const res = await fetch('https://api.example.com/users');
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return <UserTable users={users} />;
}
// components/UserTable.js (Client)
'use client';
export default function UserTable({ users }) {
const [sortBy, setSortBy] = useState('name');
const sortedUsers = [...users].sort((a, b) =>
a[sortBy].localeCompare(b[sortBy])
);
return (
<table>
{/* Interactive sorting */}
</table>
);
}
Pattern 3: Use Context Providers Wisely
Context providers must be client components, but you can wrap them strategically.
// app/layout.js (Server)
import { ThemeProvider } from '@/components/ThemeProvider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
// components/ThemeProvider.js (Client)
'use client';
import { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Pro Tip: You cannot import server components into client components, but you can pass them as children or props!
Decision Tree: Which Component Type?
Use this simple decision tree:
- Need useState, useEffect, or other hooks? → Client Component
- Need event handlers (onClick, onChange)? → Client Component
- Need browser APIs (window, localStorage)? → Client Component
- Fetching data from API/database? → Server Component
- Static content with no interactivity? → Server Component
- Need to keep API keys secret? → Server Component
Common Mistakes to Avoid
Mistake 1: Using Hooks in Server Components
// Problem: Error: Can't use hooks in server components
export default function Page() {
const [data, setData] = useState([]); // Error!
return <div>{data}</div>;
}
// Solution: Use async/await instead
export default async function Page() {
const data = await fetchData();
return <div>{data}</div>;
}
Mistake 2: Making Everything a Client Component
Don't add 'use client' to everything just to avoid errors. This defeats the purpose of server components and hurts performance.
Mistake 3: Importing Server Components into Client Components
// This won't work
'use client';
import ServerComponent from './ServerComponent';
export default function ClientComponent() {
return <ServerComponent />; // Error!
}
// Solution: Pass as children instead
'use client';
export default function ClientComponent({ children }) {
return <div>{children}</div>;
}
Migrate to Next.js with Confidence
ReactToNext automatically handles server/client component conversion, making your migration smooth and error-free.
Try ReactToNext Free →
Conclusion
Understanding server vs client components is fundamental to building modern Next.js applications. The key is to:
- Use server components by default for better performance
- Add
'use client' only when you need interactivity
- Keep client components small and focused
- Compose both types strategically for optimal results
With practice, choosing the right component type becomes second nature, and you'll build faster, more efficient applications.