AiTechWorlds
AiTechWorlds
Server Components are the biggest architectural shift in React since hooks. Understanding them at a mental-model level — not just syntax — is what makes Next.js App Router click. Most developers struggle because they try to learn rules before they understand the why.
Traditional React apps (Create React App, Vite) have a fundamental issue: everything runs in the browser.
User requests page → Browser gets empty HTML → Downloads JS bundle →
React runs in browser → Renders components → User sees content
Problems with this:
Server Components run on the server. They render to HTML before the browser gets involved.
Imagine your app split into two distinct environments:
┌─────────────────────────────────┐
│ SERVER │
│ - Has database access │
│ - Has secrets/API keys │
│ - No interactivity │
│ - Renders to HTML │
│ - Server Components live here │
└────────────────┬────────────────┘
│ HTML + RSC payload
┌────────────────▼────────────────┐
│ BROWSER │
│ - Has interactivity │
│ - Has event handlers │
│ - Has state (useState) │
│ - No database/secrets │
│ - Client Components live here │
└─────────────────────────────────┘
Server Components: Render on server, generate HTML. Can access databases directly. Cannot use hooks, event handlers, or browser APIs.
Client Components: Run in browser. Have interactivity. Cannot access database directly.
By default, every component is a Server Component.
You opt into a Client Component with 'use client' at the top of the file:
// ProductPage.tsx — Server Component (default, no directive)
import { db } from '@/lib/db'; // Direct database access ✓
async function ProductPage({ params }: { params: { id: string } }) {
// Fetch data directly — no API route needed!
const product = await db.query(
'SELECT * FROM products WHERE id = ?',
[params.id]
);
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<AddToCartButton productId={product.id} /> {/* Client Component */}
</div>
);
}
// AddToCartButton.tsx — Client Component
'use client';
import { useState } from 'react';
function AddToCartButton({ productId }: { productId: number }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => {
addToCart(productId);
setAdded(true);
}}>
{added ? '✓ Added!' : 'Add to Cart'}
</button>
);
}
You can render Client Components inside Server Components. The key: data flows down, not up.
// Server Component — can do async, DB queries, access secrets
async function Dashboard() {
const [user, stats, recentOrders] = await Promise.all([
db.getUser(userId),
db.getStats(userId),
db.getRecentOrders(userId, 10),
]);
return (
<div className="dashboard">
<UserHeader user={user} /> {/* Could be Server Component */}
<StatsGrid stats={stats} /> {/* Server Component */}
<InteractiveChart data={stats.chartData} /> {/* Client Component — has hover/filter */}
<OrderTable orders={recentOrders} /> {/* Server Component */}
<NotificationBell userId={user.id} /> {/* Client Component — has real-time */}
</div>
);
}
The result: most of the page is static HTML from the server. Only the interactive parts ship JavaScript.
| Capability | Server Component | Client Component |
|---|---|---|
async/await (top level) | ✓ | ✗ |
| Direct database queries | ✓ | ✗ |
| Access env variables (secrets) | ✓ | Only public ones |
useState / useEffect hooks | ✗ | ✓ |
| Event handlers (onClick, etc.) | ✗ | ✓ |
| Browser APIs (window, document) | ✗ | ✓ |
| Render Server Components as children | ✓ | Only via children prop |
| Import Client Components | ✓ | ✓ |
Client Components can contain Server Components if they receive them as children:
// 'use client' — this is a Client Component
'use client';
function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && <div className="modal">{children}</div>}
</div>
);
}
// Server Component — renders a Client Component with Server Component children
async function Page() {
const data = await db.getExpensiveData(); // Server-side
return (
<Modal>
<DataDisplay data={data} /> {/* Server Component as children */}
</Modal>
);
}
DataDisplay is still a Server Component — it rendered on the server. Modal is a Client Component with interactivity. They work together.
A traditional React app for a dashboard:
Bundle: React (45KB) + App (120KB) + Chart library (80KB) + Data grid (95KB) = ~340KB
Time to interactive: ~3-5 seconds on mobile
With Server Components:
Server renders: Dashboard HTML + StatsGrid HTML + OrderTable HTML
Bundle: React (45KB) + AddToCartButton (2KB) + InteractiveChart (80KB) = ~127KB
Time to interactive: <1 second on mobile (HTML already there)
Only interactive components need to ship JavaScript. Read-only content becomes pure HTML.
Ask yourself: Does this component need to be interactive?
useState, useEffect, or event handlers? → Client ComponentRule of thumb: Push 'use client' as deep in the component tree as possible. The more Server Components you have, the better your performance.
Next lesson: When to Use Client Components — a practical decision guide for the boundary between server and client.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises