1. Server Components vs Client Components
| Features | Server Component (default) | Client Component ("use client") |
|---|---|---|
| Rendering | Server only | Server (SSR) + Client (hydration) |
| JavaScript bundle | 0 KB (no JS sent) | Included in bundle |
| Data fetching | Direct DB/API access | useEffect, TanStack Query |
| State/Effects | ❌ No useState/useEffect | ✅ Full hooks support |
| Event handlers | ❌ No onClick, onChange | ✅ Full event support |
| Browser APIs | ❌ No windows, documents | ✅ Full browser access |
| Secrets/env | ✅ Safe to use | ❌ Exposed to clients |
2. When to use what?
Server Component (default): Client Component ("use client"):
✅ Static content ✅ useState, useEffect
✅ Data fetching ✅ onClick, onChange
✅ Access database/API ✅ Browser APIs
✅ Sensitive logic ✅ Custom hooks with state
✅ Large dependencies ✅ Third-party libs (charts, editors)
3. Composition Patterns
// ✅ Pattern 1: Server Component wraps Client Component
// app/dashboard/page.tsx (Server Component)
import { InteractiveChart } from './InteractiveChart';
export default async function Dashboard() {
const data = await fetchAnalytics(); // Server-side data fetching
return (
<div>
<h1>Dashboard</h1>
<InteractiveChart data={data} /> {/* Pass serializable data */}
</div>
);
}
// InteractiveChart.tsx
'use client';
export function InteractiveChart({ data }: { data: ChartData[] }) {
const [filter, setFilter] = useState('all');
// Interactive client-side logic
return <div>{/* chart */}</div>;
}
// ✅ Pattern 2: Children pattern (Server inside Client)
// ClientLayout.tsx
'use client';
export function ClientLayout({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div className="flex">
<Sidebar open={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
<main>{children}</main> {/* children can be Server Components! */}
</div>
);
}
// page.tsx (Server Component)
import { ClientLayout } from './ClientLayout';
export default async function Page() {
const data = await fetchData(); // Server-side
return (
<ClientLayout>
<ServerContent data={data} /> {/* Server component as children */}
</ClientLayout>
);
}
// ❌ Anti-pattern: Importing Server Component INTO Client Component
'use client';
import ServerComponent from './ServerComponent'; // ⚠️ Becomes client component!
// ✅ Fix: Pass as children or props
4. Streaming & Suspense
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Mỗi Suspense boundary stream independently */}
<Suspense fallback={<CardSkeleton />}>
<RevenueChart /> {/* Async server component */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders /> {/* Async server component */}
</Suspense>
<Suspense fallback={<ListSkeleton />}>
<TopProducts /> {/* Async server component */}
</Suspense>
</div>
);
}
// Async Server Component
async function RevenueChart() {
const data = await fetchRevenue(); // Can take 2-3 seconds
return <Chart data={data} />;
}
async function RecentOrders() {
const orders = await fetchRecentOrders(); // Independent fetch
return <OrderTable orders={orders} />;
}
5. Serialization Rules
// Server → Client props must be serializable:
// ✅ string, number, boolean, null, undefined
// ✅ Array, plain Object
// ✅ Date (serialized as string)
// ❌ Functions (cannot pass onClick from server)
// ❌ Class instances
// ❌ Symbols
// ❌ DOM nodes
// ✅ OK
<ClientComponent data={jsonData} count={42} />
// ❌ Error
<ClientComponent onClick={() => {}} ref={myRef} />
Next article: Data Fetching & Caching — server-side fetching, ISR, revalidation, and caching strategies.