DEV Community

myougaTheAxo
myougaTheAxo

Posted on

React Server Components Patterns: RSC Design with Claude Code

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)