React Server Components
Full Reference: See advanced.md for streaming patterns, caching strategies, advanced composition patterns, error handling, and parallel/sequential data fetching.
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:react, topic:server-componentsfor comprehensive documentation.
When NOT to Use This Skill
- Building client-only apps (CRA, Vite)
- Using Next.js Pages Router
- React version < 18
- Framework doesn't support RSC
Overview
┌─────────────────────────────────────────────────────────────────┐
│ Server │
├─────────────────────────────────────────────────────────────────┤
│ Server Components │
│ • Run only on server │
│ • Can access databases, filesystems, secrets │
│ • Zero bundle size impact │
│ • No hooks (useState, useEffect) │
├─────────────────────────────────────────────────────────────────┤
│ Client │
├─────────────────────────────────────────────────────────────────┤
│ Client Components │
│ • Run on client (and server for SSR) │
│ • Can use hooks, event handlers │
│ • Interactive │
│ • Add to JS bundle │
└─────────────────────────────────────────────────────────────────┘
Server Components (Default in Next.js App Router)
// app/users/page.tsx - Server Component by default
import { db } from '@/lib/db';
export default async function UsersPage() {
// Direct database access - no API layer needed!
const users = await db.user.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
});
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Accessing Server-Only Resources
// app/dashboard/page.tsx
import { cookies, headers } from 'next/headers';
export default async function DashboardPage() {
const cookieStore = await cookies();
const token = cookieStore.get('session')?.value;
const user = await verifyToken(token);
if (!user) {
redirect('/login');
}
const stats = await db.stats.findUnique({
where: { userId: user.id },
});
return <Dashboard user={user} stats={stats} />;
}
Client Components
Add 'use client' directive for interactivity:
// components/counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
Composition: Server + Client
// app/products/page.tsx (Server Component)
import { db } from '@/lib/db';
import { FilterSidebar } from '@/components/filter-sidebar';
export default async function ProductsPage({ searchParams }) {
const products = await db.product.findMany({
where: { category: searchParams.category },
});
const categories = await db.category.findMany();
return (
<div className="flex">
{/* Client Component for interactivity */}
<FilterSidebar categories={categories} />
{/* Server-rendered products */}
<ProductGrid products={products} />
</div>
);
}
Server Actions
Mutate data from client with server functions:
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10),
});
export async function createPost(formData: FormData) {
const validated = CreatePostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!validated.success) {
return { error: validated.error.flatten().fieldErrors };
}
const user = await getCurrentUser();
if (!user) {
return { error: { auth: ['Unauthorized'] } };
}
const post = await db.post.create({
data: { ...validated.data, authorId: user.id },
});
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
}
Using Server Actions
// Option 1: In a form
export default function CreatePostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}
// Option 2: With useActionState for loading/error states
'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';
export function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, null);
return (
<form action={formAction}>
<input name="title" required />
{state?.error?.title && <p className="error">{state.error.title[0]}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
When to Use Server vs Client
| Server Components | Client Components |
|---|---|
| Data fetching | Event handlers (onClick, onChange) |
| Database access | useState, useEffect |
| Sensitive logic (secrets) | Browser APIs |
| Heavy dependencies | Interactivity |
| Static content | Real-time updates |
// Decision tree:
// 1. Does it need interactivity? → Client
// 2. Does it use hooks? → Client
// 3. Does it need browser APIs? → Client
// 4. Everything else → Server (default)
Anti-Patterns
| Anti-Pattern | Why It's Bad | Correct Approach |
|---|---|---|
| Making everything Client Component | Loses RSC benefits | Default to Server, client only when needed |
| Passing functions to Client Components | Not serializable | Use Server Actions |
| Fetching data in Client Components | Slower, larger bundle | Fetch in Server Components |
| Not using Suspense | Sequential loading | Add Suspense boundaries |
| Exposing secrets in Client | Security risk | Keep in Server Components |
Quick Troubleshooting
| Issue | Likely Cause | Solution |
|---|---|---|
| "use client" not working | Wrong file location | Add to top of file |
| Cannot pass function as prop | Functions not serializable | Use Server Action |
| Hooks error in Server Component | Using client hooks | Add "use client" directive |
| Data not updating | Cache not revalidated | Use revalidatePath or revalidateTag |
| Slow data fetching | Sequential fetches | Use parallel Promise.all |
Best Practices
- ✅ Default to Server Components
- ✅ Use
'use client'only when needed - ✅ Keep Client Components small and focused
- ✅ Pass Server Components as children to Client
- ✅ Use Suspense for streaming
- ✅ Use Server Actions for mutations
- ❌ Don't fetch data in Client Components if avoidable
- ❌ Don't pass functions as props to Client Components