What Are React Server Components
React Server Components (RSC) execute components on the server side and send serialized results to the client. In Next.js 13+ App Router, components are server components by default.
The biggest benefit of RSC is bundle size reduction. Libraries used only on the server aren't included in client JS. Data fetching can also run directly on the server, eliminating waterfalls.
However, getting the Server/Client boundary design wrong leads to unintended hydration errors or sensitive data leaking into client code.
Server/Client Boundary Design
// app/products/page.tsx - Server Component (default)
import { db } from "@/lib/db";
import { ProductCard } from "./ProductCard";
import { AddToCartButton } from "./AddToCartButton";
export default async function ProductsPage() {
// Direct DB access on server - no API route needed
const products = await db.product.findMany({
orderBy: { createdAt: "desc" },
take: 20,
});
return (
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<div key={product.id}>
{/* Server Component: pass DB data directly */}
<ProductCard product={product} />
{/* Client Component: needs interaction */}
<AddToCartButton productId={product.id} price={product.price} />
</div>
))}
</div>
);
}
// app/products/AddToCartButton.tsx - Client Component
"use client";
import { useState } from "react";
import { addToCart } from "@/app/actions";
export function AddToCartButton({ productId, price }: { productId: string; price: number }) {
const [loading, setLoading] = useState(false);
const [added, setAdded] = useState(false);
const handleClick = async () => {
setLoading(true);
await addToCart(productId);
setAdded(true);
setLoading(false);
};
return (
<button onClick={handleClick} disabled={loading || added}>
{loading ? "Adding..." : added ? "Added to Cart" : `$${price} Add to Cart`}
</button>
);
}
Server Actions
// app/actions.ts
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
export async function addToCart(productId: string): Promise<void> {
const session = await auth();
if (!session?.user?.id) {
throw new Error("Login required");
}
await db.cartItem.upsert({
where: { userId_productId: { userId: session.user.id, productId } },
create: { userId: session.user.id, productId, quantity: 1 },
update: { quantity: { increment: 1 } },
});
revalidatePath("/cart");
}
Data Fetching Patterns: Parallel, Sequential, Streaming
// app/dashboard/page.tsx
import { Suspense } from "react";
export default async function DashboardPage() {
// Fetch independent data in parallel
const [stats, recentOrders] = await Promise.all([
getUserStats(),
getRecentOrders({ limit: 5 }),
]);
return (
<div>
<StatsPanel stats={stats} />
<RecentOrders orders={recentOrders} />
{/* Heavy data: lazy load with Suspense */}
<Suspense fallback={<RecommendationsSkeleton />}>
<RecommendationsSection />
</Suspense>
</div>
);
}
// Loads independently without blocking the full page
async function RecommendationsSection() {
const recommendations = await getRecommendations();
return <RecommendationsList items={recommendations} />;
}
Cache Strategy
import { unstable_cache as cache } from "next/cache";
const getPopularProducts = cache(
async (categoryId: string) => {
return await db.product.findMany({
where: { categoryId, isPublished: true },
orderBy: { salesCount: "desc" },
take: 10,
});
},
["popular-products"],
{ revalidate: 3600, tags: ["products"] },
);
// Invalidate specific tag cache from Server Action
import { revalidateTag } from "next/cache";
export async function publishProduct(productId: string) {
await db.product.update({ where: { id: productId }, data: { isPublished: true } });
revalidateTag("products");
}
Error Handling and Loading States
// app/products/error.tsx
"use client";
export default function ProductsError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Failed to load products</h2>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
);
}
// app/products/loading.tsx
export default function ProductsLoading() {
return (
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="skeleton-card animate-pulse">
<div className="h-48 bg-gray-200 rounded" />
</div>
))}
</div>
);
}
The core of RSC design is "make only the minimum unit that needs interaction a Client Component." Mastering this principle is the key to the design.
This article is from the Claude Code Complete Guide (7 chapters) on note.com.
myouga (@myougatheaxo) - VTuber axolotl. Sharing practical AI development tips.
Top comments (0)