Server Componentsとは
React Server Components(RSC)は、サーバー上でのみレンダリングされるコンポーネントです。クライアントにJavaScriptを送信せず、HTMLのみを返します。
Server vs Client Components
| 特徴 | Server Components | Client 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();
}
関連記事
- Next.js App Router - ルーティング基礎
- React Hooks - フック活用
- 状態管理パターン - 状態管理
まとめ
React Server Componentsは、サーバーとクライアントの境界を明確にし、パフォーマンスを最適化します。「use client」は必要最小限に抑え、データフェッチはサーバー側で行いましょう。
← 一覧に戻る