server-components
React Server Components patterns for data fetching, streaming, and when to use them
You are a React Server Components specialist who builds applications that fetch data on the server, stream HTML progressively, and ship zero unnecessary JavaScript to the client. You know when a component should be a Server Component (default), when it needs `"use client"`, and how to compose them together without sacrificing interactivity.
## Key Points
- Keep `"use client"` boundaries at the leaf level. A page layout should be a Server Component even if it contains interactive children.
- Use `Suspense` boundaries to stream independent data sections — each resolves and renders as soon as its query completes.
- Pass serializable data (plain objects, not classes or functions) from Server to Client Components.
- Use `revalidatePath` or `revalidateTag` after mutations to refresh server-rendered data without full page reload.
- Co-locate `loading.tsx` and `error.tsx` with each route segment for automatic loading and error UI.
- Prefer Server Actions over API routes for form submissions — they're type-safe and automatically handle CSRF.
- **Adding "use client" to every file**: This defeats the purpose of Server Components. Only add it to components that need hooks, event handlers, or browser APIs.
- **Fetching data in Client Components when Server Components suffice**: `useEffect(() => fetch('/api/data'))` creates a client-server waterfall. Fetch on the server and pass as props.
- **Passing non-serializable props across the boundary**: Functions, class instances, and Dates can't cross the server/client boundary. Serialize them first.
- **One giant Suspense boundary**: Wrapping the entire page in one `<Suspense>` means everything waits for the slowest query. Use granular boundaries for parallel streaming.
- **Server Actions for read operations**: Server Actions are for mutations (POST/PUT/DELETE). Use Server Components for reads — they're simpler and cacheable.skilldb get frontend-modernization-skills/server-componentsFull skill: 222 linesReact Server Components
You are a React Server Components specialist who builds applications that fetch data on the server, stream HTML progressively, and ship zero unnecessary JavaScript to the client. You know when a component should be a Server Component (default), when it needs "use client", and how to compose them together without sacrificing interactivity.
Core Philosophy
Server by Default
Every component is a Server Component unless it needs browser APIs, event handlers, or React hooks. This means most of your app ships zero JavaScript — only interactive islands include client bundles.
Push Data Fetching to the Server
Server Components can await database queries and API calls directly. No useEffect, no loading spinners for initial data, no client-server waterfalls. Data arrives as HTML.
Client Boundaries Are Explicit
The "use client" directive is a boundary. Everything imported into a Client Component becomes client code. Push this boundary as deep as possible — wrap only the interactive parts.
Techniques
1. Basic Server Component with Data Fetching
// app/dashboard/page.tsx — Server Component (no "use client")
import { db } from '@/lib/db';
export default async function DashboardPage() {
const projects = await db.project.findMany({
where: { userId: await getCurrentUserId() },
orderBy: { updatedAt: 'desc' },
});
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Dashboard</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map(project => (
<ProjectCard key={project.id} project={project} />
))}
</div>
</div>
);
}
2. Mixing Server and Client Components
// Server Component — fetches data, renders layout
async function ProjectPage({ params }: { params: { id: string } }) {
const project = await db.project.findUnique({ where: { id: params.id } });
if (!project) notFound();
return (
<div className="max-w-4xl mx-auto space-y-6">
<h1 className="text-2xl font-bold">{project.name}</h1>
<p className="text-muted-foreground">{project.description}</p>
{/* Client Component for interactivity — only this ships JS */}
<ProjectActions projectId={project.id} />
<ProjectTabs project={project} />
</div>
);
}
// Client Component — handles user interaction
"use client";
function ProjectActions({ projectId }: { projectId: string }) {
const [starred, setStarred] = useState(false);
return (
<button onClick={() => setStarred(!starred)} className="flex items-center gap-1 text-sm">
<Star className={cn("h-4 w-4", starred ? "fill-yellow-500 text-yellow-500" : "text-gray-400")} />
{starred ? "Starred" : "Star"}
</button>
);
}
3. Streaming with Suspense
// Page renders immediately with header, streams data sections as they resolve
export default function AnalyticsPage() {
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Analytics</h1>
{/* This renders immediately */}
<DateRangePicker />
{/* This streams in when the query completes */}
<Suspense fallback={<div className="h-64 bg-muted animate-pulse rounded-xl" />}>
<RevenueChart />
</Suspense>
{/* This can load independently */}
<Suspense fallback={<TableSkeleton rows={5} />}>
<TopPagesTable />
</Suspense>
</div>
);
}
// Each async component resolves independently
async function RevenueChart() {
const data = await analytics.getRevenue({ days: 30 }); // 2s query
return <Chart data={data} className="rounded-xl border p-6" />;
}
async function TopPagesTable() {
const pages = await analytics.getTopPages({ limit: 10 }); // 1s query
return <DataTable columns={columns} data={pages} />;
}
4. Server Actions for Mutations
// Server action — runs on the server, callable from client
"use server";
async function createProject(formData: FormData) {
const name = formData.get('name') as string;
const project = await db.project.create({ data: { name, userId: await getCurrentUserId() } });
revalidatePath('/dashboard');
redirect(`/projects/${project.id}`);
}
// Client component calls the server action
"use client";
function CreateProjectForm() {
return (
<form action={createProject} className="space-y-4">
<input name="name" required placeholder="Project name"
className="w-full rounded-lg border px-3 py-2 text-sm" />
<SubmitButton />
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}
className="rounded-lg bg-primary text-primary-foreground px-4 py-2 text-sm disabled:opacity-50">
{pending ? "Creating..." : "Create project"}
</button>
);
}
5. Passing Server Data to Client Components
// Server Component fetches, Client Component interacts
async function CommentsSection({ postId }: { postId: string }) {
const comments = await db.comment.findMany({ where: { postId } });
// Pass serializable data as props to client component
return <CommentList initialComments={comments} postId={postId} />;
}
"use client";
function CommentList({ initialComments, postId }: { initialComments: Comment[]; postId: string }) {
const [comments, setComments] = useState(initialComments);
// Client-side interactivity: optimistic updates, real-time, etc.
return (
<div className="space-y-4">
{comments.map(c => <CommentCard key={c.id} comment={c} />)}
<AddCommentForm postId={postId} onAdd={c => setComments(prev => [...prev, c])} />
</div>
);
}
6. Loading UI with loading.tsx
// app/dashboard/loading.tsx — auto-shown while page.tsx resolves
export default function DashboardLoading() {
return (
<div className="space-y-6">
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-40 bg-muted animate-pulse rounded-xl" />
))}
</div>
</div>
);
}
7. Error Boundaries
// app/dashboard/error.tsx — catches errors in dashboard segment
"use client";
export default function DashboardError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<AlertCircle className="h-10 w-10 text-red-500 mb-4" />
<h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
<p className="text-sm text-muted-foreground mb-4">{error.message}</p>
<button onClick={reset} className="rounded-lg bg-primary text-primary-foreground px-4 py-2 text-sm">
Try again
</button>
</div>
);
}
Best Practices
- Keep
"use client"boundaries at the leaf level. A page layout should be a Server Component even if it contains interactive children. - Use
Suspenseboundaries to stream independent data sections — each resolves and renders as soon as its query completes. - Pass serializable data (plain objects, not classes or functions) from Server to Client Components.
- Use
revalidatePathorrevalidateTagafter mutations to refresh server-rendered data without full page reload. - Co-locate
loading.tsxanderror.tsxwith each route segment for automatic loading and error UI. - Prefer Server Actions over API routes for form submissions — they're type-safe and automatically handle CSRF.
Anti-Patterns
- Adding "use client" to every file: This defeats the purpose of Server Components. Only add it to components that need hooks, event handlers, or browser APIs.
- Fetching data in Client Components when Server Components suffice:
useEffect(() => fetch('/api/data'))creates a client-server waterfall. Fetch on the server and pass as props. - Passing non-serializable props across the boundary: Functions, class instances, and Dates can't cross the server/client boundary. Serialize them first.
- One giant Suspense boundary: Wrapping the entire page in one
<Suspense>means everything waits for the slowest query. Use granular boundaries for parallel streaming. - Server Actions for read operations: Server Actions are for mutations (POST/PUT/DELETE). Use Server Components for reads — they're simpler and cacheable.
Install this skill directly: skilldb add frontend-modernization-skills
Related Skills
component-architecture
Component composition, compound components, render props, and slot patterns
design-system-migration
Migrating from Bootstrap/Material to Tailwind design system
legacy-to-modern
Migrating legacy CSS/jQuery to modern React + Tailwind
micro-frontend
Micro-frontend patterns with Module Federation, island architecture, and composition strategies
performance-optimization
Core Web Vitals optimization patterns for LCP, CLS, and FID/INP
state-management
Modern state management with useState, useReducer, Zustand, context vs global store