Server Components are great for static content and data fetching, but what about buttons, forms, and animations? That's where Client Components come in. By adding the 'use client' directive at the top of a file, you tell Next.js "this component needs to run in the browser." Client Components can use hooks, handle events, and access browser APIs—everything you need for interactivity. Let's learn when and how to use them effectively!
What Are Client Components?
Client Components are React components that run in the browser:
- Marked with 'use client': Directive at top of file
- Include JavaScript: Sent to client bundle
- Full React features: Hooks, event handlers, browser APIs
- Interactive: Enable user interaction and state
- Hydrated: Start as HTML, then become interactive
Client Components in Your App
Client Components are marked with 'use client' directive
Select a file or folder to see details
The 'use client' Directive
'use client' is a module directive, not a React API. It tells the bundler: "this module and its imports need to be in the client bundle."
Creating Your First Client Component
Basic Client Component
'use client'; // This makes it a Client Component!
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>
);
}
// ✅ Has 'use client' directive
// ✅ Uses useState hook
// ✅ Has onClick event handler
// ✅ Sent to client bundleWithout 'use client' (Won't Work!)
// ❌ NO 'use client' - This will ERROR!
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0); // ❌ Error: Server Components can't use hooks
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}> {/* ❌ Error: No event handlers */}
Increment
</button>
</div>
);
}
// ❌ Missing 'use client'
// ❌ Treated as Server Component
// ❌ Can't use hooks or event handlers⚡ Directive Placement
'use client' must be at the very top of the file, before any imports. It's the first thing in your file.
What Can Client Components Do?
✅ Client Components CAN:
- Use React hooks - useState, useEffect, useContext, etc.
- Handle events - onClick, onChange, onSubmit, etc.
- Access browser APIs - window, localStorage, geolocation, etc.
- Use Context - create and consume Context providers
- Use browser-only libraries - chart libraries, UI libraries
- Track user interactions - analytics, click tracking
❌ Client Components CANNOT:
- Be async functions - no async/await in component
- Access backend directly - must use API routes
- Use server-only APIs - no fs, database, etc.
- Import Server Components - can only pass as children
'use client';
import { useState, useEffect } from 'react';
// ✅ GOOD: Client Component doing client things
export function GoodClientComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// Access browser API
localStorage.setItem('count', count.toString());
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
// ❌ BAD: Client Component trying to be async
export async function BadClientComponent() { // ❌ Can't be async!
const data = await fetch('...');
return <div>{data}</div>;
}
// ✅ GOOD: Use hooks for async operations
export function GoodAsyncClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(r => r.json())
.then(setData);
}, []);
return <div>{data?.title}</div>;
}Common Client Component Use Cases
1. Interactive Buttons
'use client';
import { useState } from 'react';
export default function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(0);
const handleLike = async () => {
setLiked(!liked);
setCount(liked ? count - 1 : count + 1);
// Call API to save like
await fetch('/api/like', {
method: 'POST',
body: JSON.stringify({ postId, liked: !liked }),
});
};
return (
<button
onClick={handleLike}
className={liked ? 'text-red-500' : 'text-gray-500'}
>
❤️ {count} Likes
</button>
);
}2. Forms with Validation
'use client';
import { useState } from 'react';
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validate = () => {
const newErrors: Record<string, string> = {};
if (!formData.name) newErrors.name = 'Name is required';
if (!formData.email) newErrors.email = 'Email is required';
if (!/S+@S+.S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.message) newErrors.message = 'Message is required';
return newErrors;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setIsSubmitting(true);
try {
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData),
});
alert('Message sent!');
setFormData({ name: '', email: '', message: '' });
} catch (error) {
alert('Failed to send message');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Your name"
className="w-full px-4 py-2 border rounded"
/>
{errors.name && <p className="text-red-500 text-sm">{errors.name}</p>}
</div>
<div>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Your email"
className="w-full px-4 py-2 border rounded"
/>
{errors.email && <p className="text-red-500 text-sm">{errors.email}</p>}
</div>
<div>
<textarea
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
placeholder="Your message"
className="w-full px-4 py-2 border rounded"
rows={4}
/>
{errors.message && <p className="text-red-500 text-sm">{errors.message}</p>}
</div>
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-3 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
}3. Search with Live Results
'use client';
import { useState, useEffect } from 'react';
interface SearchResult {
id: string;
title: string;
description: string;
}
export default function SearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const searchTimeout = setTimeout(async () => {
setIsSearching(true);
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
setResults(data.results);
} catch (error) {
console.error('Search failed:', error);
} finally {
setIsSearching(false);
}
}, 300); // Debounce: wait 300ms after user stops typing
return () => clearTimeout(searchTimeout);
}, [query]);
return (
<div className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="w-full px-4 py-2 border rounded"
/>
{isSearching && (
<div className="absolute right-3 top-3">
<div className="animate-spin h-5 w-5 border-2 border-blue-600 rounded-full border-t-transparent" />
</div>
)}
{results.length > 0 && (
<div className="absolute w-full mt-2 bg-white border rounded-lg shadow-lg">
{results.map(result => (
key={result.id}
href={`/search/${result.id}`}
className="block p-4 hover:bg-gray-50"
>
<h3 className="font-semibold">{result.title}</h3>
<p className="text-sm text-gray-600">{result.description}</p>
</a>
))}
</div>
)}
</div>
);
}4. Modal Dialog
'use client';
import { useEffect } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
export default function Modal({ isOpen, onClose, children }: ModalProps) {
useEffect(() => {
// Close on escape key
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
// Prevent body scroll
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={onClose}
>
{/* Backdrop */}
<div className="absolute inset-0 bg-black/50" />
{/* Modal */}
<div
className="relative bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
>
✕
</button>
{children}
</div>
</div>
);
}5. Dark Mode Toggle
'use client';
import { useState, useEffect } from 'react';
export default function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
// Check localStorage on mount
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const shouldBeDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
setIsDark(shouldBeDark);
// Apply theme
document.documentElement.classList.toggle('dark', shouldBeDark);
}, []);
const toggleTheme = () => {
const newIsDark = !isDark;
setIsDark(newIsDark);
// Save to localStorage
localStorage.setItem('theme', newIsDark ? 'dark' : 'light');
// Apply theme
document.documentElement.classList.toggle('dark', newIsDark);
};
return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700"
aria-label="Toggle theme"
>
{isDark ? '🌙' : '☀️'}
</button>
);
}Using Client Components with Server Components
The most common pattern: Server Component fetches data, Client Component adds interactivity:
Server Component (Page)
import { LikeButton } from '@/components/LikeButton';
import { CommentForm } from '@/components/CommentForm';
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`);
return res.json();
}
// Server Component - fetches data
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
return (
<article>
{/* Server-rendered content */}
<h1>{post.title}</h1>
<p className="text-gray-600">By {post.author}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Client Component for interactivity */}
<LikeButton postId={post.id} initialLikes={post.likes} />
{/* Another Client Component */}
<CommentForm postId={post.id} />
</article>
);
}
// ✅ Server Component fetches data
// ✅ Client Components add interactivity
// ✅ Best of both worlds!Client Component (LikeButton)
'use client';
import { useState } from 'react';
export function LikeButton({
postId,
initialLikes,
}: {
postId: string;
initialLikes: number;
}) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(false);
const handleLike = async () => {
const newLiked = !liked;
setLiked(newLiked);
setLikes(newLiked ? likes + 1 : likes - 1);
await fetch('/api/like', {
method: 'POST',
body: JSON.stringify({ postId, liked: newLiked }),
});
};
return (
<button
onClick={handleLike}
className={`flex items-center gap-2 px-4 py-2 rounded ${
liked ? 'bg-red-100 text-red-600' : 'bg-gray-100'
}`}
>
❤️ {likes} Likes
</button>
);
}Perfect Combination
This pattern gives you the best of both worlds:
- Server Component fetches data (fast, SEO-friendly)
- Client Component adds interactivity (buttons, forms)
- Most of your page is Server Components (small bundle)
- Only interactive parts are Client Components
Client Component Boundary Rules
Rule 1: Server Components Can Import Client Components
// ✅ ALLOWED: Server Component importing Client Component
import { Counter } from './Counter'; // Has 'use client'
export default async function Page() {
const data = await getData();
return (
<div>
<h1>{data.title}</h1>
<Counter /> {/* Client Component */}
</div>
);
}Rule 2: Client Components CANNOT Import Server Components
'use client';
// ❌ ERROR: Client Component cannot import Server Component
import { BlogPost } from './BlogPost'; // Server Component
export function MyComponent() {
return <BlogPost />; // ❌ Won't work!
}Rule 3: But Client Components Can Receive Server Components as Children
// Server Component
async function Page() {
const post = await getPost();
return (
<ClientWrapper>
{/* Pass Server Component as children */}
<BlogPost post={post} /> {/* Server Component */}
</ClientWrapper>
);
}
// Client Component
'use client';
export function ClientWrapper({ children }) {
return (
<div className="wrapper">
{children} {/* Server Component rendered as child */}
</div>
);
}Rule 4: 'use client' Marks the Boundary
// File A: Has 'use client'
'use client';
import { ComponentB } from './ComponentB';
export function ComponentA() {
return <ComponentB />;
}
// File B: No 'use client'
// BUT becomes Client Component because imported by Client Component!
export function ComponentB() {
return <div>I'm also a Client Component now</div>;
}
// Once you cross the 'use client' boundary,
// everything below is a Client Component!Client Component Best Practices
1. Use Client Components Sparingly
Only add 'use client' where you actually need interactivity. Keep as much as possible as Server Components.
// ❌ BAD: Entire page is Client Component
'use client';
export default function Page() {
const [count, setCount] = useState(0);
return (
<div>
<Header /> {/* Doesn't need to be client */}
<Content /> {/* Doesn't need to be client */}
<button onClick={() => setCount(count + 1)}>
{count}
</button>
</div>
);
}
// ✅ GOOD: Only interactive part is Client Component
export default function Page() {
return (
<div>
<Header /> {/* Server Component */}
<Content /> {/* Server Component */}
<Counter /> {/* Client Component */}
</div>
);
}2. Push 'use client' Down
Move the 'use client' directive to the lowest component that needs it:
// ❌ BAD: Too high up
'use client'; // Entire file is client now
export function ProductPage({ product }) {
return (
<div>
<ProductDetails product={product} /> {/* Now client */}
<ProductImages images={product.images} /> {/* Now client */}
<AddToCartButton productId={product.id} /> {/* Needs to be client */}
</div>
);
}
// ✅ GOOD: Only the button needs to be client
// ProductPage.tsx (Server Component)
export function ProductPage({ product }) {
return (
<div>
<ProductDetails product={product} /> {/* Server */}
<ProductImages images={product.images} /> {/* Server */}
<AddToCartButton productId={product.id} /> {/* Client */}
</div>
);
}
// AddToCartButton.tsx
'use client';
export function AddToCartButton({ productId }) {
// Only this component is Client Component
}3. Pass Data as Props, Not Context
// ❌ BAD: Using Context from Server Component
// Won't work - Server Components can't use Context
// ✅ GOOD: Pass data as props
// Server Component
async function Page() {
const user = await getUser();
return (
<ClientComponent user={user} /> {/* Pass as prop */}
);
}
// Client Component
'use client';
export function ClientComponent({ user }) {
return <div>Welcome, {user.name}!</div>;
}4. Minimize Client Bundle Size
'use client';
// ❌ BAD: Heavy import in Client Component
import _ from 'lodash'; // 70KB!
export function MyComponent() {
const sorted = _.sortBy(data, 'name');
return <div>{sorted}</div>;
}
// ✅ GOOD: Use lightweight alternative
export function MyComponent() {
const sorted = data.sort((a, b) => a.name.localeCompare(b.name));
return <div>{sorted}</div>;
}
// Or extract heavy work to Server Component5. Use Server Actions for Mutations
// Server Action
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title');
await db.posts.create({ title });
}
// Client Component using Server Action
'use client';
import { createPost } from './actions';
export function PostForm() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
// ✅ Form in Client Component
// ✅ Mutation on server
// ✅ No API route neededComplete Practical Example
Interactive Search Bar (Client Component)
A complete search bar with debouncing and live results
Output Preview
Key Takeaways
- Add 'use client' at top of file - before any imports
- Client Components enable interactivity - hooks, events, browser APIs
- Cannot be async - use useEffect for data fetching
- Server Components can import Client Components - common pattern
- Client Components cannot import Server Components - but can receive as children
- Use sparingly - only where interactivity is needed
- Push 'use client' down - keep as much as Server Components
- Client Components are added to bundle - minimize size
What's Next?
You now understand both Server and Client Components! But when should you use each? The next lesson provides a decision guide to help you choose between Server and Client Components for any situation.
We'll cover specific scenarios, common patterns, and a decision tree to make the choice clear. You'll learn to architect your application for optimal performance while maintaining the interactivity users expect.
🎯 Start with Server, Add Client
The golden rule: start with Server Components for everything. Only add 'use client' when you need interactivity. This approach gives you the best performance by default!