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
- Your component code
- Your data fetching
- Your state management
- All of it ships to the user’s device
Next.js 14 App Router: Most things run on the server
- Components render on the server
- Data fetches happen on the server
- Only interactive pieces run in the browser
This means:
- Smaller JavaScript bundles
- Faster initial page loads
- Better SEO
- Reduced client-side data fetching
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:
- Fetch data directly with async/await
- Access databases
- Read environment variables
- Use server-only libraries
- Keep sensitive logic secure
What you CANNOT do:
- Use hooks like
useState,useEffect - Add event listeners like
onClick - Access browser APIs
- Use browser-only libraries
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:
- Interactive UI (clicks, hovers, form inputs)
- React hooks (useState, useEffect, useContext)
- Browser-only APIs (localStorage, window, document)
- Event listeners
- Third-party libraries that need the browser
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):
- JavaScript bundle: 245 KB
- Time to Interactive: 3.2s
- First Contentful Paint: 1.8s
New approach (Server + Client Components):
- JavaScript bundle: 87 KB
- Time to Interactive: 1.4s
- First Contentful Paint: 0.6s
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)
- YES → Client Component
- NO → Keep as Server Component
Does it use React hooks?
- YES → Client Component
- NO → Keep as Server Component
Does it need browser APIs? (localStorage, window, etc.)
- YES → Client Component
- NO → Keep as Server Component
None of the above?
- Keep as Server Component (it’s faster!)
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:
- Data fetching happens on the server (faster, more secure)
- Less JavaScript ships to users (faster load times)
- You only use Client Components when you need interactivity
Start with Server Components. Add ‘use client’ only when you need it. Your users will thank you with their faster load times.