Server & Client Components
Understand React Server Components and when to use client components
React Server Components (RSC)
Next.js uses React Server Components by default in the App Router. Server Components render on the server, reduce client-side JavaScript, and can directly access backend resources like databases and file systems.
π₯οΈ Server Components (Default)
- β Fetch data directly (no useEffect)
- β Access backend resources (DB, filesystem)
- β Keep sensitive info on server (API keys)
- β Reduce client bundle size
- β Cannot use hooks (useState, useEffect)
- β Cannot use browser APIs
- β Cannot add event listeners
π» Client Components ('use client')
- β Use React hooks (useState, useEffect)
- β Add event listeners (onClick, onChange)
- β Access browser APIs (localStorage, window)
- β Use third-party hooks libraries
- β Cannot be async functions
- β Increase client bundle size
Server Component Example
Server Components can be async, fetch data directly, and keep secrets on the server while returning only HTML to the client.
// app/users/page.tsx - Server Component (default)
// No 'use client' directive needed
import { db } from '@/lib/database';
// This component runs only on the server
export default async function UsersPage() {
// Direct database access - no API needed!
const users = await db.user.findMany();
// Async/await works directly in the component
const stats = await fetch('https://api.example.com/stats', {
headers: {
// Safe to use - never sent to client
Authorization: `Bearer ${process.env.SECRET_API_KEY}`,
},
}).then(res => res.json());
return (
<div>
<h1>Users ({stats.total})</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
// Zero client-side JavaScript for this component!
Client Component Example
Client Components opt into browser APIs and hooks, so theyβre ideal for interactive UI and stateful behavior.
'use client'; // This directive makes it a Client Component
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const [mounted, setMounted] = useState(false);
// useEffect works in Client Components
useEffect(() => {
setMounted(true);
// Access browser APIs
const saved = localStorage.getItem('count');
if (saved) setCount(parseInt(saved));
}, []);
const increment = () => {
setCount(c => c + 1);
localStorage.setItem('count', String(count + 1));
};
if (!mounted) return null; // Avoid hydration mismatch
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
Composition Patterns
Prefer wrapping Server Components with a small Client Component to keep most of the tree server-rendered and avoid bloating the client bundle.
// β
CORRECT: Pass Server Components as children
// app/dashboard/page.tsx (Server Component)
import ClientWrapper from './ClientWrapper';
import ServerData from './ServerData';
export default async function Dashboard() {
return (
<ClientWrapper>
{/* ServerData stays a Server Component */}
<ServerData />
</ClientWrapper>
);
}
// ClientWrapper.tsx
'use client';
export default function ClientWrapper({
children
}: {
children: React.ReactNode
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
);
}
// β WRONG: Importing Server Component into Client Component
'use client';
import ServerComponent from './ServerComponent'; // This becomes a Client Component!
When to Use Each
| Use Case | Component Type |
|---|---|
| Fetch data | π₯οΈ Server |
| Access backend resources | π₯οΈ Server |
| Keep sensitive data secure | π₯οΈ Server |
| Add interactivity (onClick, onChange) | π» Client |
| Use state (useState, useReducer) | π» Client |
| Use lifecycle effects (useEffect) | π» Client |
| Use browser-only APIs | π» Client |
| Use React Context | π» Client |
Mixing Components
Keep client interactivity at the leaves so data-heavy components remain server-rendered.
// Best Practice: Keep Client Components at the leaves
// β BAD: Large Client Component wrapping everything
'use client';
export default function Page() {
const [filter, setFilter] = useState('');
return (
<div>
<input onChange={e => setFilter(e.target.value)} />
<DataTable filter={filter} /> {/* Now forced to be client */}
</div>
);
}
// β
GOOD: Small Client Component for interactivity only
// SearchFilter.tsx
'use client';
export function SearchFilter({ onFilter }: { onFilter: (v: string) => void }) {
return <input onChange={e => onFilter(e.target.value)} />;
}
// page.tsx (Server Component)
import { SearchFilter } from './SearchFilter';
import { DataTable } from './DataTable'; // Stays Server Component
export default async function Page() {
const data = await fetchData();
return (
<div>
<SearchFilter onFilter={handleFilter} />
<DataTable data={data} />
</div>
);
}
Third-Party Libraries
Many UI libraries depend on hooks, so wrap them in a small Client Component and pass data from the server.
// Many libraries use hooks and need 'use client'
// Create wrapper components for third-party components
// components/ChartWrapper.tsx
'use client';
import { LineChart } from 'some-chart-library';
export function ChartWrapper({ data }: { data: number[] }) {
return <LineChart data={data} />;
}
// Use in Server Component
import { ChartWrapper } from './ChartWrapper';
export default async function Dashboard() {
const data = await fetchChartData(); // Server-side fetch
return <ChartWrapper data={data} />; // Client-side render
}
β‘ Key Takeaways
- β’ Default to Server Components - only use 'use client' when needed
- β’ Keep Client Components small and at the leaf nodes
- β’ Pass Server Components as children to Client Components
- β’ Don't import Server Components into Client Components
- β’ Wrap third-party libraries that need hooks