Article

Server Components vs Client Components: A Practical Guide to Next.js

5 min read
Originally published on Medium. Read, clap or comment on Medium ↗

The Big Change Nobody Warned You About

If you learned React in 2022 or earlier, Next.js 14’s App Router might feel like learning React all over again. Components are now “Server Components” by default. The useState hook doesn't work anymore. You're seeing weird errors about "use client".

What’s going on?

This isn’t Next.js making things complicated — it’s actually making your apps faster and more efficient. Once you understand the pattern, you’ll wonder how you ever built apps the old way.

The Mental Model: Where Does Your Code Run?

Traditional React: Everything runs in the browser

Next.js 14 App Router: Most things run on the server

This means:

Server Components: The New Default

In the App Router, every component is a Server Component unless you say otherwise.

// app/posts/page.js
// This is a Server Component (no directive needed)
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function Posts() {
const posts = await getPosts();

return (
<div>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}

What you can do in Server Components:

What you CANNOT do:

Client Components: For Interactivity

When you need interactivity, add 'use client' at the top:

// app/components/LikeButton.js
'use client';
import { useState } from 'react';export default function LikeButton() {
const [likes, setLikes] = useState(0);

return (
<button onClick={() => setLikes(likes + 1)}>
❤️ {likes} likes
</button>
);
}

When to use Client Components:

The Best of Both Worlds

Here’s where it gets powerful: you can mix them!

// app/posts/[id]/page.js (Server Component)
async function getPost(id) {
const res = await fetch(`https://api.example.com/posts/${id}`);
return res.json();
}
import LikeButton from '@/app/components/LikeButton';
import Comments from '@/app/components/Comments';export default async function Post({ params }) {
const post = await getPost(params.id);

return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>

{/* This is a Client Component for interactivity */}
<LikeButton />

{/* This is also a Client Component */}
<Comments postId={post.id} />
</article>
);
}

The server fetches the data, renders most of the page, and only the interactive bits run in the browser. Your user downloads way less JavaScript!

Real-World Example: A Dashboard

Let’s build a dashboard that shows this pattern in action.

// app/dashboard/page.js (Server Component)
async function getAnalytics() {
// This runs on the server - database credentials are safe
const data = await db.query('SELECT * FROM analytics');
return data;
}
import Chart from './Chart';
import FilterBar from './FilterBar';export default async function Dashboard() {
const analytics = await getAnalytics();

return (
<div>
<h1>Dashboard</h1>

{/* Server Component - just displays data */}
<div className="stats">
<div>Total Users: {analytics.totalUsers}</div>
<div>Revenue: ${analytics.revenue}</div>
</div>

{/* Client Component - needs interactivity */}
<FilterBar />

{/* Client Component - interactive chart */}
<Chart data={analytics.chartData} />
</div>
);
}
// app/dashboard/Chart.js (Client Component)
'use client';
import { useState } from 'react';
import { LineChart } from 'recharts';export default function Chart({ 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>
<option value="year">Last Year</option>
</select>

<LineChart data={data} />
</div>
);
}

Common Patterns and Solutions

Pattern 1: Fetching Data in a Client Component

'use client';
import { useEffect, useState } from 'react';export default function UserProfile() {
const [user, setUser] = useState(null);

useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser);
}, []);

if (!user) return <div>Loading...</div>;

return <div>Hello, {user.name}!</div>;
}

Better approach: Fetch in Server Component, pass to Client

// app/profile/page.js (Server)
async function getUser() {
const res = await fetch('https://api.example.com/user');
return res.json();
}
import UserProfile from './UserProfile';export default async function ProfilePage() {
const user = await getUser();
return <UserProfile user={user} />;
}// app/profile/UserProfile.js (Client)
'use client';import { useState } from 'react';export default function UserProfile({ user }) {
const [isEditing, setIsEditing] = useState(false);

return (
<div>
<h1>{user.name}</h1>
<button onClick={() => setIsEditing(!isEditing)}>
Edit Profile
</button>
{isEditing && <EditForm user={user} />}
</div>
);
}

Pattern 2: Shared State Between Components

Use Context, but be smart about it:

// app/providers.js
'use client';
import { createContext, useState } from 'react';export const CartContext = createContext();export function CartProvider({ children }) {
const [cart, setCart] = useState([]);

return (
<CartContext.Provider value={{ cart, setCart }}>
{children}
</CartContext.Provider>
);
}
// app/layout.js (Server)
import { CartProvider } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<CartProvider>
{children}
</CartProvider>
</body>
</html>
);
}

Performance Comparison

I rebuilt a blog using both approaches:

Old approach (all Client Components):

New approach (Server + Client Components):

That’s a 64% reduction in JavaScript and pages load 56% faster!

Decision Tree: Server or Client?

Ask yourself these questions:

Does it need interactivity? (clicks, input, hover)

Does it use React hooks?

Does it need browser APIs? (localStorage, window, etc.)

None of the above?

Common Mistakes

Mistake 1: Making everything a Client Component

// ❌ Don't do this
'use client';
export default async function Posts() {
const posts = await fetch('/api/posts');
// ...
}

Mistake 2: Prop drilling from Server to Client to Server

This doesn’t work. Once you enter a Client Component, all children are Client Components too.

Mistake 3: Importing Server-only code in Client Components

// ❌ This will error
'use client';
import db from '@/lib/database'; // Server-only!

The Bottom Line

Server Components aren’t about making things harder — they’re about making your apps faster by default. The framework does the heavy lifting:

Start with Server Components. Add ‘use client’ only when you need it. Your users will thank you with their faster load times.

← All articles Discuss on Medium ↗