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>
 );
}

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

実践メモ: データフェッチはPromise.allで並列実行し、ウォーターフォール(直列実行)を避けましょう。レスポンス時間が大幅に短縮されます。

並列フェッチ

// 並列で実行
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>
 );
}

ポイント: 'use client'は必要最小限のコンポーネントにのみ付与してください。Server Componentをデフォルトとし、インタラクティブな部分だけをClient Componentに分離するのが最適な設計です。

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

// ❌ 悪い例: 全体を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>;
}

注意: ServerからClient Componentへのpropsはシリアライズ可能なデータのみ渡せます。関数、Date、Map、Setなどは直接渡せません。

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」は必要最小限に抑え、データフェッチはサーバー側で行いましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR