React Server Components実践ガイド

中級 | 30分 で読める | 2025.01.10

Server Componentsとは

React Server Components(RSC)は、サーバー上でのみレンダリングされるコンポーネントです。クライアントにJavaScriptを送信せず、HTMLのみを返します。

Server vs Client Components

特徴Server ComponentsClient Components
レンダリングサーバーブラウザ
バンドルサイズ含まれない含まれる
データアクセス直接DB/APIアクセス可fetch経由
状態管理useState不可useState可
イベントonClick等不可onClick等可

基本的な使い方

Server Component

// app/products/page.tsx(デフォルトでServer Component)
import { db } from '@/lib/db';

async function ProductList() {
  // 直接データベースにアクセス
  const products = await db.product.findMany({
    orderBy: { createdAt: 'desc' },
  });

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name} - ¥{product.price}
        </li>
      ))}
    </ul>
  );
}

export default function ProductsPage() {
  return (
    <main>
      <h1>商品一覧</h1>
      <ProductList />
    </main>
  );
}

Client Component

// components/AddToCartButton.tsx
'use client';  // クライアントコンポーネントを明示

import { useState } from 'react';

export function AddToCartButton({ productId }: { productId: string }) {
  const [isLoading, setIsLoading] = useState(false);

  const handleClick = async () => {
    setIsLoading(true);
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId }),
    });
    setIsLoading(false);
  };

  return (
    <button onClick={handleClick} disabled={isLoading}>
      {isLoading ? '追加中...' : 'カートに追加'}
    </button>
  );
}

組み合わせ

// app/products/[id]/page.tsx
import { db } from '@/lib/db';
import { AddToCartButton } from '@/components/AddToCartButton';

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({
    where: { id: params.id },
  });

  if (!product) return <div>商品が見つかりません</div>;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>¥{product.price}</p>
      {/* Client Componentを埋め込み */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

データフェッチングパターン

並列フェッチ

// 並列で実行
async function Dashboard() {
  const [user, orders, notifications] = await Promise.all([
    getUser(),
    getOrders(),
    getNotifications(),
  ]);

  return (
    <div>
      <UserProfile user={user} />
      <OrderList orders={orders} />
      <NotificationList notifications={notifications} />
    </div>
  );
}

Suspenseでストリーミング

import { Suspense } from 'react';

async function SlowComponent() {
  const data = await slowFetch();  // 3秒かかる
  return <div>{data}</div>;
}

export default function Page() {
  return (
    <div>
      <h1>ダッシュボード</h1>  {/* 即座に表示 */}

      <Suspense fallback={<Skeleton />}>
        <SlowComponent />  {/* 遅延表示 */}
      </Suspense>
    </div>
  );
}

ネストしたSuspense

export default function Page() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>

      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>

      <Suspense fallback={<ContentSkeleton />}>
        <MainContent />
      </Suspense>
    </div>
  );
}

Server Actionsとの連携

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function createProduct(formData: FormData) {
  const name = formData.get('name') as string;
  const price = Number(formData.get('price'));

  await db.product.create({
    data: { name, price },
  });

  revalidatePath('/products');
}
// app/products/new/page.tsx
import { createProduct } from '@/app/actions';

export default function NewProductPage() {
  return (
    <form action={createProduct}>
      <input name="name" placeholder="商品名" required />
      <input name="price" type="number" placeholder="価格" required />
      <button type="submit">作成</button>
    </form>
  );
}

コンポーネント境界の設計

// ❌ 悪い例: 全体をClient Componentに
'use client';

export default function ProductPage() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <ProductDetails />  {/* サーバーで取得できるのに... */}
      <Counter count={count} setCount={setCount} />
    </div>
  );
}

// ✅ 良い例: 最小限のClient Component
// ProductPage.tsx (Server Component)
export default async function ProductPage() {
  const product = await getProduct();

  return (
    <div>
      <ProductDetails product={product} />
      <Counter />  {/* これだけClient */}
    </div>
  );
}

// Counter.tsx
'use client';
export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

propsの受け渡し

// Server → Client へのprops渡し
// シリアライズ可能なデータのみ

// ✅ OK
<ClientComponent
  data={{ name: 'Product', price: 1000 }}
  items={['a', 'b', 'c']}
/>

// ❌ NG(関数は渡せない)
<ClientComponent onClick={() => console.log('click')} />

// ❌ NG(Dateはそのまま渡せない)
<ClientComponent date={new Date()} />

// ✅ 解決策
<ClientComponent date={new Date().toISOString()} />

キャッシュ戦略

// fetch のキャッシュ制御
async function getProduct(id: string) {
  // デフォルト: キャッシュされる
  const res = await fetch(`/api/products/${id}`);

  // キャッシュなし
  const res = await fetch(`/api/products/${id}`, { cache: 'no-store' });

  // 時間ベースの再検証
  const res = await fetch(`/api/products/${id}`, {
    next: { revalidate: 60 },  // 60秒
  });

  return res.json();
}

関連記事

まとめ

React Server Componentsは、サーバーとクライアントの境界を明確にし、パフォーマンスを最適化します。「use client」は必要最小限に抑え、データフェッチはサーバー側で行いましょう。

← 一覧に戻る