You know what Server and Client Components are. Now let's master when to use each. This isn't just about technical capabilities—it's about making architectural decisions that optimize for performance, user experience, and developer productivity. Should this be a Server Component or Client Component? By the end of this lesson, you'll have a clear decision framework, practical examples, and the confidence to architect Next.js applications that are both fast and interactive.
The Golden Rule
🎯 Default to Server Components
Start with Server Components for everything.
Only add 'use client' when you need:
- Interactivity (clicks, inputs, state)
- React hooks (useState, useEffect, etc.)
- Browser APIs (localStorage, window, etc.)
- Event listeners (onClick, onChange, etc.)
This simple rule gives you the best performance by default.
Quick Decision Matrix
| Task / Feature | Component Type | Why? |
|---|---|---|
| Fetch data from database | Server | Direct access, secure, SEO-friendly |
| Button with onClick | Client | Needs event handler |
| Display static content | Server | No interactivity needed |
| Form with validation | Client | Needs useState and onChange |
| Access environment variables | Server | Secrets stay secure |
| Use localStorage | Client | Browser API |
| Render a list from API | Server | Better performance, SEO |
| Interactive chart/graph | Client | User interaction needed |
| Process markdown | Server | Heavy library stays on server |
| Modal with animations | Client | Needs state and effects |
Decision Flowchart
Does it need interactivity?
(Buttons, forms, state, events)
✅ Server Component
- Better performance
- SEO-friendly
- Zero JavaScript to client
Does it need React hooks or browser APIs?
✅ Client Component
Add 'use client' directive
💡 Consider Server Actions
Form submissions can use Server Actions without Client Component
Scenario-Based Decisions
Scenario 1: Blog Post Page
Need: Display post content + like button + comment form
Decision:
- Page (Server Component): Fetch and display post content
- LikeButton (Client Component): Handle clicks, manage state
- CommentForm (Client Component): Form validation, submission
// 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();
}
export default async function BlogPostPage({ params }) {
const post = await getPost(params.slug);
return (
<article>
{/* Server Component: Static content */}
<h1>{post.title}</h1>
<div className="prose" dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Client Component: Interactive */}
<LikeButton postId={post.id} initialLikes={post.likes} />
{/* Client Component: Interactive */}
<CommentForm postId={post.id} />
</article>
);
}
// ✅ Server Component fetches data
// ✅ Client Components add interactivity
// ✅ Optimal performanceScenario 2: Dashboard with Analytics
Need: Display stats + interactive charts + date range picker
Decision:
- Page (Server Component): Fetch stats from database
- StatCards (Server Component): Display static stats
- Chart (Client Component): Interactive chart with tooltips
- DatePicker (Client Component): Date selection
// Server Component - Page
import { db } from '@/lib/database';
import { Chart } from '@/components/Chart';
import { DatePicker } from '@/components/DatePicker';
async function getStats() {
return await db.query('SELECT * FROM analytics');
}
export default async function DashboardPage() {
const stats = await getStats();
return (
<div>
{/* Server Component: Static stats */}
<div className="grid grid-cols-3 gap-6">
<StatCard title="Revenue" value={`$${stats.revenue}`} />
<StatCard title="Users" value={stats.users} />
<StatCard title="Orders" value={stats.orders} />
</div>
{/* Client Component: Date picker */}
<DatePicker />
{/* Client Component: Interactive chart */}
<Chart data={stats.chartData} />
</div>
);
}
// Server Component for static UI
function StatCard({ title, value }) {
return (
<div className="bg-white p-6 rounded shadow">
<h3 className="text-gray-600">{title}</h3>
<p className="text-3xl font-bold">{value}</p>
</div>
);
}Scenario 3: E-commerce Product Page
Need: Product details + image gallery + add to cart
Decision:
- Page (Server Component): Fetch product data
- ProductInfo (Server Component): Display details
- ImageGallery (Client Component): Interactive carousel
- AddToCart (Client Component): Cart interaction
// Server Component - Page
import { ImageGallery } from '@/components/ImageGallery';
import { AddToCart } from '@/components/AddToCart';
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<div className="grid grid-cols-2 gap-8">
{/* Client Component: Interactive gallery */}
<ImageGallery images={product.images} />
<div>
{/* Server Component: Static content */}
<h1 className="text-4xl font-bold">{product.title}</h1>
<p className="text-3xl text-green-600">${product.price}</p>
<p className="text-gray-700">{product.description}</p>
{/* Server Component: Static specs */}
<div className="mt-6">
<h3 className="font-bold mb-2">Specifications</h3>
<ul>
{product.specs.map(spec => (
<li key={spec.key}>{spec.key}: {spec.value}</li>
))}
</ul>
</div>
{/* Client Component: Interactive button */}
<AddToCart product={product} />
</div>
</div>
);
}Real-World Component Decisions
See how different features require different component types
Select a file or folder to see details
Common Patterns
Pattern 1: Server Component Wrapper
Server Component fetches data, passes to Client Component:
// Server Component
async function Page() {
const data = await fetchData();
return <ClientComponent data={data} />;
}
// Client Component
'use client';
export function ClientComponent({ data }) {
const [selected, setSelected] = useState(null);
// Use data with interactivity
}Pattern 2: Composition (Passing Children)
When Client Component needs Server Component children:
// Server Component
async function Page() {
const post = await getPost();
return (
<ClientWrapper>
{/* Server Component passed as child */}
<ServerContent post={post} />
</ClientWrapper>
);
}
// Client Component receives Server Component as children
'use client';
export function ClientWrapper({ children }) {
const [expanded, setExpanded] = useState(false);
return (
<div className={expanded ? 'expanded' : 'collapsed'}>
{children}
</div>
);
}Pattern 3: Island Architecture
Small Client Components (islands) in a sea of Server Components:
// Server Component - Page
export default async function Page() {
return (
<article>
{/* Server: Static header */}
<Header />
{/* Server: Static content */}
<Content />
{/* Client: Interactive island */}
<LikeButton />
{/* Server: Static content */}
<RelatedPosts />
{/* Client: Interactive island */}
<CommentForm />
{/* Server: Static footer */}
<Footer />
</article>
);
}
// Mostly Server Components with strategic Client ComponentsPattern 4: Shared Logic with Hooks
When you need to share logic that requires hooks:
// Custom hook (only works in Client Components)
'use client';
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Client Component using the hook
'use client';
import { useLocalStorage } from './hooks';
export function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
// ...
}Gray Areas & How to Decide
Gray Area 1: Simple Form
Scenario: A contact form without complex validation
Options:
- Server Action + Server Component: No Client Component needed
- Client Component: If you want instant validation feedback
Decision: Use Server Actions if possible (better performance). Use Client Component only if you need instant feedback or complex validation.
Gray Area 2: Data That Changes Rarely
Scenario: User preferences that update infrequently
Options:
- Server Component: Fetch on each page load
- Client Component: Fetch once, store in state
Decision: Default to Server Component with caching. Only use Client Component if the data truly needs to persist across navigation without refetch.
Gray Area 3: Animations
Scenario: Content with entrance animations
Options:
- CSS animations: Can work in Server Components
- JavaScript animations: Need Client Component
Decision: Use CSS animations in Server Components when possible. Use Client Component only for complex animations that require JavaScript (libraries like Framer Motion).
Anti-Patterns to Avoid
❌ Anti-Pattern 1: Making Everything Client
// ❌ BAD: Everything is Client Component
'use client';
export default function Page() {
return (
<div>
<Header /> {/* Doesn't need 'use client' */}
<BlogPost /> {/* Doesn't need 'use client' */}
<Footer /> {/* Doesn't need 'use client' */}
<LikeButton /> {/* Only this needs 'use client' */}
</div>
);
}
// ✅ GOOD: Only interactive parts are Client
export default function Page() {
return (
<div>
<Header /> {/* Server Component */}
<BlogPost /> {/* Server Component */}
<Footer /> {/* Server Component */}
<LikeButton /> {/* Client Component */}
</div>
);
}❌ Anti-Pattern 2: Client Component Fetching Data
// ❌ BAD: Client Component fetching data
'use client';
export function BlogPost({ slug }) {
const [post, setPost] = useState(null);
useEffect(() => {
fetch(`/api/posts/${slug}`)
.then(r => r.json())
.then(setPost);
}, [slug]);
return <div>{post?.title}</div>;
}
// ✅ GOOD: Server Component fetching data
async function BlogPost({ slug }) {
const post = await fetch(`/api/posts/${slug}`).then(r => r.json());
return <div>{post.title}</div>;
}❌ Anti-Pattern 3: Not Using Composition
// ❌ BAD: Client Component trying to import Server Component
'use client';
import { ServerComponent } from './ServerComponent';
export function ClientComponent() {
return <ServerComponent />; // Won't work!
}
// ✅ GOOD: Use composition
function Page() {
return (
<ClientComponent>
<ServerComponent /> {/* Passed as children */}
</ClientComponent>
);
}Optimization Tips
Tip 1: Minimize Client Component Tree
Place 'use client' as deep as possible:
// ❌ Suboptimal: High-level Client Component
'use client';
function ProductPage({ product }) {
return (
<div>
<ProductImage image={product.image} /> {/* Now Client */}
<ProductDetails details={product.details} /> {/* Now Client */}
<AddToCart productId={product.id} /> {/* Needs Client */}
</div>
);
}
// ✅ Optimal: Only button is Client Component
function ProductPage({ product }) {
return (
<div>
<ProductImage image={product.image} /> {/* Server */}
<ProductDetails details={product.details} /> {/* Server */}
<AddToCart productId={product.id} /> {/* Client */}
</div>
);
}Tip 2: Extract Interactive Parts
Separate interactive logic into small Client Components:
// ✅ Large Server Component with small Client Component
function ArticlePage({ article }) {
return (
<article className="prose">
{/* All Server Component */}
<h1>{article.title}</h1>
<img src={article.image} />
<div dangerouslySetInnerHTML={{ __html: article.content }} />
{/* Small Client Component for interactivity */}
<ShareButtons />
</article>
);
}Tip 3: Use Server Actions for Mutations
Avoid Client Components when Server Actions can handle it:
// ✅ Form with Server Action (no Client Component needed)
import { createPost } from './actions';
function NewPostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Create Post</button>
</form>
);
}
// actions.ts
'use server';
export async function createPost(formData: FormData) {
// Handle on server
}Key Takeaways
- Default to Server Components - best performance
- Add 'use client' only for interactivity - hooks, events, browser APIs
- Server Component for data fetching - direct access, SEO-friendly
- Client Component for user interaction - buttons, forms, state
- Compose them together - Server fetches, Client adds interactivity
- Push 'use client' down - minimize Client Component tree
- Use Server Actions when possible - avoid unnecessary Client Components
- When in doubt, start with Server - can always add Client later
What's Next?
You now have a solid decision framework for choosing between Server and Client Components! Next, we'll dive deeper into Component Composition Patterns—advanced techniques for combining Server and Client Components effectively.
You'll learn patterns like the "children prop pattern," "wrapper pattern," and "slot pattern" that let you build sophisticated architectures while maintaining optimal performance. These patterns are key to mastering Next.js 15!
🎯 Practice Makes Perfect
The best way to internalize these decisions is to practice. Build a few small projects and consciously think about each component: "Does this need interactivity? Should this be Server or Client?" Over time, it becomes second nature!