|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/server-components
$cat docs/react-server-components-in-next.js.md
updated Recentlyยท45 min readยทpublished

React Server Components in Next.js

โ—†Next.jsโ—†Reactโ—†RSCโ—†Intermediate to Advanced๐ŸŽฏFree Tools
What are React Server Components?

React Server Components (RSC) are a new component paradigm where components run exclusively on the server. They can directly access databases, file systems, and backend services without creating API endpoints. Their JavaScript is never sent to the client โ€” only the rendered output is streamed as a special RSC payload.

In Next.js, every component inside the app/ directory is a Server Component by default. You do not need to add any special directive to make a component server-only. To opt a component into client-side rendering, add 'use client' at the top of the file.

โ„น

info

Server Components are the default in the App Router. This means your application sends less JavaScript to the client by default โ€” only Client Components (with 'use client') contribute to the client bundle.
Server vs Client Components

Understanding the difference between Server and Client Components is the most important concept in the App Router. Here is a comparison:

FeatureServer ComponentClient Component
Runs onServer onlyServer (SSR) + Client (hydration)
DirectiveNone (default)'use client'
Bundle size impactZero โ€” not in client bundleIncluded in client bundle
useState / useEffectCannot useCan use
Event handlersCannot useCan use
Browser APIsCannot accessCan access
Direct DB accessCan accessCannot access
async / awaitCan use directlyCannot use directly (use in Server Components)
app/components/server-component.tsx
TSX
1// app/components/server-component.tsx
2// This is a Server Component (no directive needed)
3
4import { db } from "@/lib/database";
5
6export default async function UserProfile({ userId }: { userId: string }) {
7 // Direct database access โ€” no API route needed
8 const user = await db.user.findUnique({ where: { id: userId } });
9
10 // This JavaScript is NEVER sent to the client
11 return (
12 <div className="p-4 border rounded">
13 <h2>{user.name}</h2>
14 <p>{user.email}</p>
15 </div>
16 );
17}
app/components/client-counter.tsx
TSX
1// app/components/client-counter.tsx
2"use client"; // Opt into client-side rendering
3
4import { useState } from "react";
5
6export default function Counter() {
7 const [count, setCount] = useState(0);
8
9 // This component is sent to the client as JavaScript
10 // It hydrates on the client to become interactive
11 return (
12 <div className="p-4 border rounded">
13 <p className="text-sm mb-2">Count: {count}</p>
14 <div className="flex gap-2">
15 <button
16 onClick={() => setCount(c => c - 1)}
17 className="px-3 py-1 bg-gray-700 rounded text-sm"
18 >
19 -
20 </button>
21 <button
22 onClick={() => setCount(c => c + 1)}
23 className="px-3 py-1 bg-[#00FF41] text-black rounded text-sm"
24 >
25 +
26 </button>
27 </div>
28 </div>
29 );
30}
โš 

warning

Do not add 'use client' to every component. Start with Server Components and only opt into Client Components when you need interactivity, browser APIs, or state management. Every Client Component adds JavaScript to the client bundle.
Composition Patterns

The key to effective RSC usage is composition. Server Components can render Client Components, and Client Components can import Server Components as children or via the children prop. But Client Components cannot import Server Components directly โ€” they must receive them as props.

Pattern 1: Server renders Client

A Server Component imports and renders a Client Component. The Server Component can fetch data and pass it as props.

app/dashboard/page.tsx
TSX
1// app/dashboard/page.tsx (Server Component)
2import { db } from "@/lib/database";
3import { LikeButton } from "./like-button"; // Client
4
5export default async function DashboardPage() {
6 const posts = await db.post.findMany();
7
8 return (
9 <div>
10 <h1>Dashboard</h1>
11 {posts.map(post => (
12 <article key={post.id}>
13 <h2>{post.title}</h2>
14 <p>{post.content}</p>
15 {/* Server passes data to Client Component */}
16 <LikeButton postId={post.id} initialLikes={post.likes} />
17 </article>
18 ))}
19 </div>
20 );
21}
app/dashboard/like-button.tsx
TSX
1// app/dashboard/like-button.tsx (Client Component)
2"use client";
3
4import { useState } from "react";
5
6export function LikeButton({
7 postId,
8 initialLikes,
9}: {
10 postId: string;
11 initialLikes: number;
12}) {
13 const [likes, setLikes] = useState(initialLikes);
14
15 async function handleLike() {
16 setLikes(l => l + 1);
17 await fetch(`/api/posts/${postId}/like`, { method: "POST" });
18 }
19
20 return (
21 <button onClick={handleLike} className="text-sm text-gray-400">
22 โ™ฅ {likes}
23 </button>
24 );
25}

Pattern 2: Server passes children to Client

A Client Component receives Server Components as children. This allows you to keep the interactive wrapper client-side while the content is server-rendered.

app/components/tabs.tsx
TSX
1// app/components/tabs.tsx (Client Component)
2"use client";
3
4import { useState } from "react";
5
6export function Tabs({
7 items,
8}: {
9 items: { label: string; content: React.ReactNode }[];
10}) {
11 const [active, setActive] = useState(0);
12
13 return (
14 <div>
15 <div className="flex gap-4 border-b mb-4">
16 {items.map((item, i) => (
17 <button
18 key={item.label}
19 onClick={() => setActive(i)}
20 className={i === active ? "border-b-2 border-[#00FF41]" : ""}
21 >
22 {item.label}
23 </button>
24 ))}
25 </div>
26 <div>{items[active].content}</div>
27 </div>
28 );
29}
app/dashboard/page.tsx
TSX
1// app/dashboard/page.tsx (Server Component)
2import { Tabs } from "@/components/tabs";
3import { OverviewTab } from "./overview-tab"; // Server
4import { AnalyticsTab } from "./analytics-tab"; // Server
5import { SettingsTab } from "./settings-tab"; // Server
6
7export default async function DashboardPage() {
8 return (
9 <Tabs
10 items={[
11 { label: "Overview", content: <OverviewTab /> },
12 { label: "Analytics", content: <AnalyticsTab /> },
13 { label: "Settings", content: <SettingsTab /> },
14 ]}
15 />
16 );
17}
๐Ÿ”ฅ

pro tip

This pattern is extremely powerful. The Tabs component manages active tab state on the client, but the tab content is rendered on the server. The tab content never needs to hydrate โ€” it is static HTML delivered from the server.

Pattern 3: Separating Client Islands

Keep Client Components as small and focused as possible. A page can be mostly Server Components with tiny Client Component islands for interactive elements.

app/products/page.tsx
TSX
1// app/products/page.tsx (Server Component)
2import { db } from "@/lib/database";
3import { ProductCard } from "./product-card"; // Server
4import { AddToCartButton } from "./add-to-cart"; // Client
5import { ProductFilters } from "./filters"; // Client
6import { ProductGrid } from "./product-grid"; // Server
7
8export default async function ProductsPage({
9 searchParams,
10}: {
11 searchParams: Promise<{ category?: string }>;
12}) {
13 const { category } = await searchParams;
14 const products = await db.product.findMany({
15 where: category ? { category } : undefined,
16 });
17
18 return (
19 <div className="flex gap-6">
20 {/* Client island โ€” handles filter interactions */}
21 <aside className="w-64">
22 <ProductFilters selected={category} />
23 </aside>
24
25 {/* Server-rendered grid */}
26 <ProductGrid>
27 {products.map(product => (
28 <ProductCard key={product.id} product={product}>
29 {/* Tiny client island โ€” just the button */}
30 <AddToCartButton productId={product.id} />
31 </ProductCard>
32 ))}
33 </ProductGrid>
34 </div>
35 );
36}
Streaming & Suspense

Streaming allows the server to send HTML progressively. Instead of waiting for all data to load before sending anything, the server sends the shell of the page immediately and streams in slow data as it resolves. This dramatically improves Time to First Byte (TTFB).

app/dashboard/page.tsx
TSX
1// app/dashboard/page.tsx
2import { Suspense } from "react";
3import { SlowAnalytics } from "./slow-analytics";
4import { FastStats } from "./fast-stats";
5import { RecentActivity } from "./recent-activity";
6
7export default function DashboardPage() {
8 return (
9 <div>
10 {/* This renders immediately */}
11 <h1>Dashboard</h1>
12
13 {/* Fast data โ€” renders in the shell */}
14 <FastStats />
15
16 {/* Slow data โ€” streams in when ready */}
17 <Suspense fallback={<div className="h-64 bg-gray-800 animate-pulse rounded" />}>
18 <SlowAnalytics />
19 </Suspense>
20
21 {/* Another slow section โ€” streams independently */}
22 <Suspense fallback={<div className="h-32 bg-gray-800 animate-pulse rounded" />}>
23 <RecentActivity />
24 </Suspense>
25 </div>
26 );
27}
app/dashboard/slow-analytics.tsx
TSX
1// app/dashboard/slow-analytics.tsx (Server Component)
2import { db } from "@/lib/database";
3
4export default async function SlowAnalytics() {
5 // This takes 3 seconds to resolve
6 const analytics = await db.analytics.aggregate({
7 period: "30d",
8 delay: 3000, // Simulated slow query
9 });
10
11 return (
12 <div className="grid grid-cols-3 gap-4 p-4 border rounded">
13 <div>
14 <p className="text-sm text-gray-500">Total Views</p>
15 <p className="text-2xl font-mono">{analytics.totalViews}</p>
16 </div>
17 <div>
18 <p className="text-sm text-gray-500">Unique Visitors</p>
19 <p className="text-2xl font-mono">{analytics.uniqueVisitors}</p>
20 </div>
21 <div>
22 <p className="text-sm text-gray-500">Bounce Rate</p>
23 <p className="text-2xl font-mono">{analytics.bounceRate}%</p>
24 </div>
25 </div>
26 );
27}
โ„น

info

The loading.tsx file in a route directory is equivalent to wrapping the entire page in a Suspense boundary. You can also use multiple Suspense boundaries within a page for more granular streaming.
Data Fetching in Server Components

Server Components can use async/await directly to fetch data. Combined with caching and Suspense, this provides a powerful data fetching model.

app/blog/page.tsx
TSX
1// app/blog/page.tsx
2import { db } from "@/lib/database";
3
4// This fetch is deduplicated per request
5async function getPosts() {
6 const posts = await db.post.findMany({
7 orderBy: { createdAt: "desc" },
8 include: { author: true, tags: true },
9 });
10 return posts;
11}
12
13// This fetch is also deduplicated โ€” same request
14async function getFeaturedPost() {
15 const post = await db.post.findFirst({
16 where: { featured: true },
17 include: { author: true },
18 });
19 return post;
20}
21
22export default async function BlogPage() {
23 // Both fetches run in parallel
24 const [posts, featured] = await Promise.all([
25 getPosts(),
26 getFeaturedPost(),
27 ]);
28
29 return (
30 <div>
31 <h1>Blog</h1>
32 {featured && <FeaturedCard post={featured} />}
33 <div className="grid grid-cols-2 gap-4">
34 {posts.map(post => (
35 <PostCard key={post.id} post={post} />
36 ))}
37 </div>
38 </div>
39 );
40}

Request Deduplication

When multiple Server Components make the same fetch request during a single render, Next.js automatically deduplicates the requests. This means if two components both call fetch("/api/data"), only one HTTP request is made. This eliminates the waterfall problem common in client-side fetching.

When to Use 'use client'

Add 'use client' only when your component needs one or more of these:

โ—†Event handlers (onClick, onSubmit, onChange)
โ—†useState, useReducer, or other React hooks
โ—†useEffect or other lifecycle hooks
โ—†Browser APIs (window, document, localStorage)
โ—†Third-party libraries that use client-only features
โ—†React Context providers (the provider itself must be a Client Component)
patterns.tsx
TSX
1// โœ… Good: Server Component with data fetching
2async function ProductList() {
3 const products = await db.product.findMany();
4 return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
5}
6
7// โœ… Good: Tiny Client Component for interactivity
8"use client";
9function AddToCartButton({ productId }: { productId: string }) {
10 const [loading, setLoading] = useState(false);
11 return (
12 <button onClick={() => { setLoading(true); addToCart(productId); }}>
13 {loading ? "Adding..." : "Add to Cart"}
14 </button>
15 );
16}
17
18// โŒ Bad: Entire page marked as Client Component
19"use client";
20export default function ProductsPage() {
21 // Now the ENTIRE page + all children are client components
22 const products = useFetch("/api/products"); // Unnecessary
23 return <ul>{products.map(p => <li>{p.name}</li>)}</ul>;
24}
โœ“

best practice

Keep the 'use client' boundary as low in the component tree as possible. The more components below the boundary, the more JavaScript is sent to the client. A good rule: if a component does not need state or event handlers, keep it as a Server Component.
Serialization Rules

When passing props from Server Components to Client Components, the data must be serializable. This means it can be converted to JSON. Functions, classes, Dates, Maps, and Sets cannot cross the server-client boundary.

TypeSerializableWorkaround
Strings, numbers, booleansYesโ€”
Plain objects, arraysYesโ€”
null, undefinedYesโ€”
Date objectsNoPass ISO string, parse in client
FunctionsNoDefine in client component or pass action reference
ClassesNoConvert to plain object
Map, Set, WeakMapNoConvert to Array or Object
app/posts/[id]/page.tsx
TSX
1// Server Component
2async function PostPage({ params }: { params: Promise<{ id: string }> }) {
3 const { id } = await params;
4 const post = await getPost(id);
5
6 // โœ… Pass serializable data to client
7 <PostActions
8 postId={post.id}
9 title={post.title}
10 createdAt={post.createdAt.toISOString()} // Date โ†’ string
11 />;
12}
Performance Benefits

Server Components provide significant performance improvements over traditional client-side React:

Zero Client Bundle for Server Components

Server Component code is never included in the client bundle. A page with complex data transformation logic, markdown parsing, or syntax highlighting adds zero bytes to the client if those components are server-only.

Reduced Waterfalls

Server-side data fetching with automatic deduplication eliminates the client waterfall. Instead of fetch โ†’ render โ†’ fetch โ†’ render on the client, the server resolves all data dependencies before sending the first byte.

Streaming for Perceived Performance

With Suspense, the shell of the page streams instantly while slow data loads. Users see content immediately instead of a loading spinner.

Smaller Hydration Payload

Only Client Components hydrate on the client. With Server Components handling most of the UI, the hydration payload is significantly smaller, leading to faster Time to Interactive (TTI).

โ„น

info

Measure the impact of Client Components by checking your bundle size. Run npm run build and look at the _next/static/chunks/ output. Each Client Component adds its code (and the code of any client-side dependencies) to the bundle.
Common Mistakes

โœ— Adding 'use client' to every component

This defeats the purpose of RSC entirely. Start with Server Components and only add the directive where needed.

โœ— Trying to use hooks in Server Components

useState, useEffect, and other hooks only work in Client Components. If you need state, create a dedicated Client Component.

โœ— Passing non-serializable props to Client Components

Functions, Date objects, and class instances cannot cross the server-client boundary. Convert them to serializable formats first.

โœ— Creating unnecessary Client Component wrappers

Do not create a Client Component that just wraps a Server Component. Pass the Server Component as children instead.

โœ“

best practice

Think of Server Components as the default and Client Components as the exception. Your component tree should be mostly Server Components with small, focused Client Components at the leaves where interactivity is needed.
$Blueprint โ€” Engineering DocumentationยทSection ID: NEXTJS-03ยทRevision: 1.0