Understanding 'use client' and 'use server' in Next.js


Next.js 13+ introduced the App Router with a revolutionary concept: Server Components by default. Understanding the difference between 'use client' and 'use server' is crucial for building efficient Next.js applications. Let's dive deep into these directives and when to use them.
By default, all components in the App Router are Server Components. They render on the server, have access to backend resources directly, and don't send JavaScript to the client.
// app/blog/page.tsx (Server Component by default)
import { db } from '@/lib/db';
export default async function BlogPage() {
// Direct database access - no API route needed!
const posts = await db.post.findMany();
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
Benefits:
Use 'use client' when your component needs:
'use client';
import { useEffect, useState } from 'react';
export default function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Window: {size.width} x {size.height}</div>;
}
'use client';
export default function Button() {
const handleClick = () => {
alert('Button clicked!');
};
return <button onClick={handleClick}>Click me</button>;
}
'use client';
import { useState, useEffect } 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>
);
}
'use client';
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
The 'use server' directive is used to create Server Actions - functions that run on the server but can be called from Client Components.
// app/actions.ts
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
}
'use client';
import { createPost } from '@/app/actions';
export default function PostForm() {
async function handleSubmit(formData: FormData) {
await createPost(formData);
}
return (
<form action={handleSubmit}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Create Post</button>
</form>
);
}
The 'use client' directive creates a boundary. Everything imported into a Client Component becomes part of the client bundle.
// ❌ Bad: Unnecessary client boundary
'use client';
import { db } from '@/lib/db'; // This won't work - db is server-only
export default function Component() {
// Only needs client for interactivity
}
// Good: Minimal client boundary
// Server Component
import { db } from '@/lib/db';
import InteractiveButton from './InteractiveButton';
export default async function Page() {
const data = await db.query(); // Server-side
return (
<div>
<h1>{data.title}</h1>
<InteractiveButton /> {/* Only this is client-side */}
</div>
);
}
// InteractiveButton.tsx
'use client';
export default function InteractiveButton() {
return <button onClick={() => alert('Clicked')}>Click</button>;
}
// Good: Small, focused client component
'use client';
export default function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
// Good: Server Component fetches data
export default async function PostList() {
const posts = await fetchPosts();
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// Server Component
export default async function Dashboard() {
const data = await fetchDashboardData();
return (
<div>
<ServerStats data={data} />
<ClientChart data={data} />
</div>
);
}
// Client Component
'use client';
import { Chart } from 'chart.js';
export default function ClientChart({ data }) {
useEffect(() => {
// Initialize chart
}, []);
return <canvas id="chart" />;
}
// Bad
'use client';
export default function StaticContent() {
return <h1>Hello World</h1>;
}
// Good
export default function StaticContent() {
return <h1>Hello World</h1>;
}
// Bad
export default function Component() {
const width = window.innerWidth; // Error: window is not defined
return <div>{width}</div>;
}
// Good
'use client';
export default function Component() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <div>{width}</div>;
}
Understanding 'use client' and 'use server' is essential for building efficient Next.js applications:
'use client' sparingly - only when you need interactivity or browser APIs'use server' for mutations - Server Actions simplify form handling and data mutationsBy following these patterns, you'll build faster, more secure, and more maintainable Next.js applications!