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
// 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
'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
// ✅ 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
// 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 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