AiTechWorlds
AiTechWorlds
Server Components are React components that render entirely on the server and ship only their finished HTML to the browser — no component JavaScript included.
Think of a restaurant menu printed fresh each morning versus a whiteboard the waiter updates live at your table. The printed menu (a Server Component) is cheap to produce and needs no ongoing attention once it reaches you. The whiteboard (a Client Component) costs more to maintain but can react to you in real time. A well-built page uses mostly printed menus, with a whiteboard only where interaction is actually needed. Most developers struggle with Server Components because they learn the rules before they understand this 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 contentProblems 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 mobileWith 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