11.2 Server Component Types — RSC vs Client Components
React Server Components (RSC) Overview
In Next.js App Router, all components are Server Components by default. Client Components require the "use client" directive at the top of the file.
| Feature | Server Component | Client Component |
|---|---|---|
| Default | ✅ | ❌ (needs "use client") |
async/await | ✅ | ❌ |
| useState/useEffect | ❌ | ✅ |
| Browser APIs | ❌ | ✅ |
| Event handlers | ❌ | ✅ |
| Direct DB access | ✅ | ❌ |
| Bundle size | ❌ (server-only) | ✅ |
Async Server Components
// app/users/page.tsx (Server Component)
import { db } from '@/lib/db';
interface User {
id: string;
name: string;
email: string;
}
// Declared as async function — runs only on server
async function UsersPage() {
// Direct DB access is possible
const users: User[] = await db.query('SELECT * FROM users');
return (
<div>
<h1>User List</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
</div>
);
}
export default UsersPage;
// Nested async server components
async function UserCard({ userId }: { userId: string }) {
const user = await fetchUser(userId);
return (
<div className="card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
async function Dashboard() {
const userIds = await getActiveUserIds();
return (
<div>
{/* Parallel rendering — more declarative than Promise.all */}
{userIds.map(id => (
<UserCard key={id} userId={id} />
))}
</div>
);
}
Client Components
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
interface CounterProps {
initialCount?: number;
label?: string;
}
export function Counter({ initialCount = 0, label = 'Counter' }: CounterProps) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>{label}: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => setCount(c => c - 1)}>-1</button>
</div>
);
}
RSC and Client Component Type Boundary
Passing Props from Server to Client Components
// ✅ Only serializable values can be passed
async function ServerPage() {
const data = await fetchData(); // { id: string, name: string, count: number }
return (
<ClientComponent
id={data.id} // ✅ string
name={data.name} // ✅ string
count={data.count} // ✅ number
// ❌ Cannot pass functions, class instances, Symbols
/>
);
}
// ❌ Wrong — functions cannot be passed
async function ServerPage() {
const handleClick = () => console.log('clicked');
return (
<ClientComponent
onClick={handleClick} // ❌ Functions cannot be serialized — error
/>
);
}
Passing Server Components as children to Client Components
// ✅ Server components can be passed as children — this works!
async function Page() {
const serverData = await fetchData();
return (
<ClientWrapper>
{/* Server component goes in as children */}
<ServerComponent data={serverData} />
</ClientWrapper>
);
}
// ClientWrapper.tsx
'use client';
import { ReactNode, useState } from 'react';
function ClientWrapper({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
);
}
Data Fetching Patterns
Sequential vs Parallel Fetching
// ❌ Sequential fetching (slow)
async function SlowPage() {
const user = await getUser(); // 1 second
const posts = await getUserPosts(user.id); // 2 seconds
// Total: 3 seconds
return <UserPosts user={user} posts={posts} />;
}
// ✅ Parallel fetching (fast)
async function FastPage({ userId }: { userId: string }) {
const [user, posts] = await Promise.all([
getUser(userId),
getUserPosts(userId),
]);
// Total: 2 seconds
return <UserPosts user={user} posts={posts} />;
}
Streaming with Suspense
import { Suspense } from 'react';
// Each component fetches data independently
async function SlowComponent() {
const data = await fetchSlowData(); // Takes 3 seconds
return <div>{data.content}</div>;
}
async function FastComponent() {
const data = await fetchFastData(); // Takes 0.5 seconds
return <div>{data.content}</div>;
}
// Each loads independently
function Page() {
return (
<div>
<Suspense fallback={<div>Loading fast data...</div>}>
<FastComponent />
</Suspense>
<Suspense fallback={<div>Loading slow data...</div>}>
<SlowComponent />
</Suspense>
</div>
);
}
cache and Server Components
import { cache } from 'react';
import { unstable_cache } from 'next/cache';
// cache(): deduplicate calls within a single request
const getUser = cache(async (id: string) => {
console.log('DB query:', id); // Runs only once even if called twice with same id
return db.user.findUnique({ where: { id } });
});
// unstable_cache(): cross-request caching (Next.js cache)
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
['user'],
{ revalidate: 60, tags: ['user'] } // Cache for 60 seconds
);
Pro Tips
Server vs Client Component Decision Pattern
Use Server Components when:
- Direct DB or API access needed
- Processing sensitive data (API keys, etc.)
- Using heavy npm packages
- Rendering static content
Use Client Components when:
- Event handlers like onClick, onChange
- React hooks like useState, useEffect
- Browser APIs like localStorage, window
- User interaction elements
Push the boundary "as far down as possible"
// ❌ Don't make the entire page a client component
'use client';
async function BadPage() { /* ... */ }
// ✅ Only make the interactive part a client component
async function GoodPage() { // Server Component
const data = await fetchData(); // Fetch data on server
return (
<div>
<StaticContent data={data} /> {/* Server Component */}
<InteractiveButton /> {/* Client Component */}
</div>
);
}