Server Components
Render React components on the server with zero client-side JavaScript -- the default in Next.js App Router.
Search across all documentation pages
Render React components on the server with zero client-side JavaScript -- the default in Next.js App Router.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card -- copy-paste ready.
// app/page.tsx -- Server Component by default (no directive needed)
import { db } from "@/lib/db";
export default async function HomePage() {
const posts = await db.post.findMany({ take: 10 });
return (
<main>
<h1>Latest Posts</h1>
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</main>
);
}When to reach for this: Any component that only reads data and renders markup -- no useState, no useEffect, no event handlers, no browser APIs. This is the default; you opt out with "use client", not in.
// app/blog/page.tsx (Server Component)
import { Suspense } from "react";
import { formatDistanceToNow } from "date-fns";
type Post = {
id: string;
title: string;
excerpt: string;
publishedAt: string;
author: { name: string; avatar: string };
};
async function fetchPosts(): Promise<Post[]> {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 300 },
});
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json();
}
export default async function BlogPage() {
return (
<main className="max-w-3xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">Blog</h1>
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>
</main>
);
}
async function PostList() {
const posts = await fetchPosts();
return (
<div className="space-y-8">
{posts.map((post) => (
<article key={post.id} className="border-b pb-6">
<h2 className="text-xl font-semibold mb-2">{post.title}</h2>
<p className="text-gray-600 mb-3">{post.excerpt}</p>
<div className="flex items-center gap-3 text-sm text-gray-500">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-6 h-6 rounded-full"
/>
<span>{post.author.name}</span>
<span>
{formatDistanceToNow(new Date(post.publishedAt), {
addSuffix: true,
})}
</span>
</div>
</article>
))}
</div>
);
}
function PostsSkeleton() {
return (
<div className="space-y-8">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="border-b pb-6">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2 animate-pulse" />
<div className="h-4 bg-gray-100 rounded w-full mb-3 animate-pulse" />
<div className="h-4 bg-gray-100 rounded w-1/2 animate-pulse" />
</div>
))}
</div>
);
}What this demonstrates:
await directly in the function bodydate-fns) that ships zero JavaScript to the client<Suspense> for streaming"use client" directive anywhere -- the entire page is server-renderedasync and use await directly. This is not allowed in Client Components.date-fns) are not included in the client bundle.revalidatePath/revalidateTag is called.Parallel data fetching:
async function Dashboard() {
const [users, revenue, orders] = await Promise.all([
fetchUsers(),
fetchRevenue(),
fetchOrders(),
]);
return (
<>
<UserTable users={users} />
<RevenueChart revenue={revenue} />
<OrderList orders={orders} />
</>
);
}Passing server data to Client Components:
// Server Component
import { ClientMap } from "./client-map";
export default async function LocationPage() {
const locations = await db.location.findMany();
// Only serializable data can cross the boundary
return <ClientMap locations={locations} />;
}Server-only utilities:
// lib/server-only-utils.ts
import "server-only"; // Throws a build error if imported in a Client Component
export function getSecretConfig() {
return {
apiKey: process.env.SECRET_API_KEY!,
dbUrl: process.env.DATABASE_URL!,
};
}// Async Server Components return Promise<JSX.Element>
// TypeScript handles this with React 19+ types
async function MyComponent(): Promise<JSX.Element> {
const data = await fetchData();
return <div>{data.name}</div>;
}
// Props must be serializable when passed to Client Components
type SerializableProps = {
name: string;
count: number;
items: { id: string; label: string }[];
// NOT allowed: onClick: () => void
// NOT allowed: ref: React.Ref<HTMLDivElement>
};
// Use `server-only` package for compile-time protection
import "server-only";Cannot use hooks -- useState, useEffect, useRef, and all other hooks are client-only. Fix: Extract interactive parts into a "use client" component.
Cannot use event handlers -- onClick, onChange, onSubmit, etc. require client-side JavaScript. Fix: Move event-handling logic to a Client Component.
Cannot access browser APIs -- window, document, localStorage, navigator are not available on the server. Fix: Use these only in "use client" components or behind typeof window !== "undefined" checks.
Props to Client Components must be serializable -- Functions (except Server Actions), class instances, Symbols, and DOM nodes cannot be passed as props across the server-client boundary. Fix: Pass only plain data; use Server Actions for callbacks.
Large server payloads -- Fetching too much data in a Server Component and passing it all as props bloats the RSC payload. Fix: Fetch only what the Client Component needs; paginate on the server.
Third-party libraries may not be RSC-compatible -- Libraries that import useState, useEffect, or browser APIs fail in Server Components. Fix: Import them only inside "use client" files, or use a wrapper component.
| Approach | Use When | Don't Use When |
|---|---|---|
| Server Components | Read-only UI, data fetching, zero-JS rendering | Interactive UI with state or effects |
| Client Components | Interactive UI with hooks, events, browser APIs | Pure data display with no interactivity |
| Server-side rendering (SSR) | Legacy pre-RSC apps that need server HTML | You have access to the App Router |
| Static Site Generation | Content rarely changes and can be built at deploy time | Data is user-specific or highly dynamic |
| API routes + client fetch | External consumers need a REST endpoint | Data is only consumed by your own pages |
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: FAQ category page with direct data access
// File: src/app/faqs/[slug]/page.tsx
export default async function FaqCategoryPage({ params }: FaqCategoryPageProps) {
const { slug } = await params;
const category = await getFaqCategory(slug);
if (!category) {
notFound();
}
const iconConfig = getFaqIcon(category.slug);
const IconComponent = iconConfig.icon;
return (
<div className="min-h-screen bg-zinc-50 dark:bg-black">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<Link href="/faqs" className="inline-flex items-center gap-2 text-sm cursor-pointer">
<ChevronLeft className="h-4 w-4" />
<span>Back to FAQs</span>
</Link>
<div className="flex items-center gap-3 mb-4">
<div className={iconConfig.color}>
<IconComponent className="h-8 w-8" />
</div>
<h1 className="text-3xl font-bold">{category.title}</h1>
</div>
<FaqList faqs={category.faqs} categorySlug={category.slug} />
</div>
</div>
);
}What this demonstrates in production:
async and directly awaits data from getFaqCategory() which calls Prisma under the hoodawait params is the Next.js 15+ pattern where params is now a Promise in dynamic routesnotFound() from next/navigation triggers the nearest not-found.tsx boundaryFaqList (a Client Component) receives pre-fetched data as props. The server/client boundary is at the prop levelComponents are Server Components by default in the App Router. No directive is needed. They become Client Components only when you add "use client" to the file.
Yes. Server Components can be async functions and use await directly in the function body. This is not allowed in Client Components.
No. Server Components produce an RSC payload (serialized React tree) that is streamed to the client. Dependencies used only in Server Components (e.g., date-fns, markdown parsers) are not included in the client bundle.
Server Components cannot use React hooks (useState, useEffect, useRef, etc.) because they run on the server and do not re-render on the client. Extract interactive parts into a "use client" component.
The library likely imports useState, useEffect, or browser APIs internally. Import it only inside a "use client" file, or create a thin Client Component wrapper around it.
Only serializable data: strings, numbers, booleans, arrays, plain objects, and Server Actions. You cannot pass regular functions, class instances, Symbols, or DOM nodes.
Use the server-only package:
import "server-only";
export function getSecretConfig() {
return { apiKey: process.env.SECRET_API_KEY! };
}This throws a build error if the file is imported in a "use client" file.
Use Promise.all to run multiple fetches concurrently:
const [users, revenue, orders] = await Promise.all([
fetchUsers(),
fetchRevenue(),
fetchOrders(),
]);Server Components do not re-render in response to client-side state changes. They re-execute only when the route changes or when revalidatePath/revalidateTag is called.
async function MyComponent(): Promise<JSX.Element> {
const data = await fetchData();
return <div>{data.name}</div>;
}React 19+ types handle Promise<JSX.Element> for async components.
Define a type with only serializable fields:
type SerializableProps = {
name: string;
count: number;
items: { id: string; label: string }[];
// NOT allowed: onClick: () => void
};"use client"Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥