はじめに
この記事では、Next.js 13で導入されたServer ComponentsとClient Componentsの違いと使い分けについて解説します。Server Componentsとは、サーバー側でレンダリングされるReactコンポーネントのことで、クライアントにJavaScriptを送信せずにHTMLを生成できます。一方、Client Componentsは、ブラウザ上で実行されるコンポーネントで、ユーザーとのインタラクションや状態管理を担当します。
Server ComponentsとClient Componentsの比較
| 特徴 | Server Components | Client Components |
|---|---|---|
| 実行場所 | サーバー | ブラウザ |
| JavaScript送信 | なし | あり |
| データ取得 | 直接可能(async/await) | useEffect/SWR等が必要 |
| 状態管理 | 不可 | useState等で可能 |
| イベントハンドラ | 不可 | onClick等で可能 |
| ブラウザAPI | 不可 | window/document等使用可能 |
| デフォルト | デフォルト | 'use client'が必要 |
Server Componentsの利点
パフォーマンスの最適化
Server Componentsは、サーバーでレンダリングされたHTMLを直接クライアントに送信するため、クライアントに送るJavaScriptを減らすことができます。これにより、ページの初期読み込み速度が向上し、ユーザー体験が改善されます。
// app/products/page.tsx(Server Component)
// async/awaitで直接データ取得可能
export default async function ProductsPage() {
// サーバーで実行されるため、APIキーは安全
const products = await fetch('https://api.example.com/products', {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
next: { revalidate: 3600 }, // 1時間キャッシュ
}).then((res) => res.json());
return (
<main>
<h1>商品一覧</h1>
<ul>
{products.map((product: { id: string; name: string; price: number }) => (
<li key={product.id}>
{product.name} - ¥{product.price.toLocaleString()}
</li>
))}
</ul>
</main>
);
}
SEO向上
サーバーでレンダリングされたHTMLは、検索エンジンに対して容易にクロールされやすく、SEOの向上にも寄与します。
バンドルサイズの削減
Server Componentsで使用するライブラリ(マークダウンパーサー、シンタックスハイライターなど)はクライアントに送信されないため、バンドルサイズを大幅に削減できます。
// app/blog/[slug]/page.tsx
import { marked } from 'marked'; // このライブラリはクライアントに送信されない
import hljs from 'highlight.js';
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
const html = marked(post.content);
return (
<article dangerouslySetInnerHTML={{ __html: html }} />
);
}
Client Componentsの利点
ユーザーインタラクションの処理
Client Componentsは、ユーザーとのインタラクションや動的な状態管理を担います。
// components/AddToCartButton.tsx
'use client';
import { useState } from 'react';
interface Props {
productId: string;
productName: string;
}
export default function AddToCartButton({ productId, productName }: Props) {
const [isAdding, setIsAdding] = useState(false);
const [isAdded, setIsAdded] = useState(false);
const handleAddToCart = async () => {
setIsAdding(true);
try {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId }),
});
setIsAdded(true);
} finally {
setIsAdding(false);
}
};
return (
<button
onClick={handleAddToCart}
disabled={isAdding || isAdded}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{isAdded ? '追加済み' : isAdding ? '追加中...' : 'カートに追加'}
</button>
);
}
ブラウザAPIの使用
windowやdocumentなどのブラウザAPIにアクセスする必要がある場合に使用します。
// components/ScrollToTop.tsx
'use client';
import { useEffect, useState } from 'react';
export default function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const toggleVisibility = () => {
setIsVisible(window.scrollY > 300);
};
window.addEventListener('scroll', toggleVisibility);
return () => window.removeEventListener('scroll', toggleVisibility);
}, []);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
if (!isVisible) return null;
return (
<button
onClick={scrollToTop}
className="fixed bottom-4 right-4 p-3 bg-gray-800 text-white rounded-full"
aria-label="トップへ戻る"
>
↑
</button>
);
}
コンポーネントの合成パターン
Server ComponentをClient Componentの子として渡す
Server ComponentをClient Componentのchildrenとして渡すことで、Server Componentの利点を維持できます。
// components/Sidebar.tsx(Client Component)
'use client';
import { useState } from 'react';
interface Props {
children: React.ReactNode;
}
export default function Sidebar({ children }: Props) {
const [isOpen, setIsOpen] = useState(true);
return (
<aside className={isOpen ? 'w-64' : 'w-0'}>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? '閉じる' : '開く'}
</button>
{isOpen && children}
</aside>
);
}
// app/layout.tsx(Server Component)
import Sidebar from '@/components/Sidebar';
import Navigation from '@/components/Navigation'; // Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<Sidebar>
{/* NavigationはServer Componentのまま */}
<Navigation />
</Sidebar>
<main>{children}</main>
</div>
);
}
インタラクティブな部分だけをClient Componentに分離
// app/products/[id]/page.tsx(Server Component)
import AddToCartButton from '@/components/AddToCartButton';
import ProductReviews from '@/components/ProductReviews';
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
const product = await getProduct(params.id);
return (
<div>
{/* 静的な部分はServer Component */}
<h1>{product.name}</h1>
<p>{product.description}</p>
<p className="text-2xl font-bold">
¥{product.price.toLocaleString()}
</p>
{/* インタラクティブな部分だけClient Component */}
<AddToCartButton
productId={product.id}
productName={product.name}
/>
{/* レビューはServer Componentで取得 */}
<ProductReviews productId={product.id} />
</div>
);
}
Suspenseによるストリーミング
Server ComponentsとSuspenseを組み合わせることで、UIを段階的にストリーミングできます。
// app/dashboard/page.tsx
import { Suspense } from 'react';
import UserStats from '@/components/UserStats';
import RecentActivity from '@/components/RecentActivity';
import Notifications from '@/components/Notifications';
export default function Dashboard() {
return (
<div className="grid grid-cols-3 gap-4">
<Suspense fallback={<div className="animate-pulse h-32 bg-gray-200" />}>
<UserStats />
</Suspense>
<Suspense fallback={<div className="animate-pulse h-32 bg-gray-200" />}>
<RecentActivity />
</Suspense>
<Suspense fallback={<div className="animate-pulse h-32 bg-gray-200" />}>
<Notifications />
</Suspense>
</div>
);
}
// components/UserStats.tsx(Server Component)
export default async function UserStats() {
// このデータ取得が完了次第、このコンポーネントがストリーミングされる
const stats = await fetchUserStats();
return (
<div className="p-4 bg-white rounded shadow">
<h2>統計情報</h2>
<p>投稿数: {stats.posts}</p>
<p>フォロワー: {stats.followers}</p>
</div>
);
}
使い分けの判断フロー
コンポーネントを作成する
│
├── useState/useEffect/useReducer を使う?
│ └── Yes → Client Component
│
├── onClick/onChange などのイベントハンドラを使う?
│ └── Yes → Client Component
│
├── window/document などのブラウザAPIを使う?
│ └── Yes → Client Component
│
├── React Context の Provider を作成する?
│ └── Yes → Client Component
│
└── 上記すべて No
└── Server Component(デフォルト)
よくある間違いと対処法
間違い1: Server ComponentでuseStateを使う
// NG: Server ComponentでuseStateは使えない
export default function Counter() {
const [count, setCount] = useState(0); // エラー
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// OK: 'use client'を追加
'use client';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
間違い2: Client ComponentからServer Componentをimport
// NG: Client ComponentからServer Componentを直接import
'use client';
import ServerComponent from './ServerComponent'; // 問題あり
export default function ClientWrapper() {
return <ServerComponent />; // Server Componentとして動作しない
}
// OK: childrenとして渡す
'use client';
export default function ClientWrapper({
children,
}: {
children: React.ReactNode;
}) {
return <div className="wrapper">{children}</div>;
}
// 親コンポーネント(Server Component)で
<ClientWrapper>
<ServerComponent />
</ClientWrapper>
間違い3: 不必要にClient Componentにする
// NG: 静的な表示だけなのにClient Component
'use client';
export default function Footer() {
return (
<footer>
<p>© 2024 Example Inc.</p>
</footer>
);
}
// OK: 'use client'を削除(Server Componentで十分)
export default function Footer() {
return (
<footer>
<p>© 2024 Example Inc.</p>
</footer>
);
}
Server Actions(簡易紹介)
Next.js 14以降では、Server Actionsを使ってClient Componentから直接サーバー関数を呼び出せます。
// app/actions.ts
'use server';
export async function submitForm(formData: FormData) {
const name = formData.get('name');
await saveToDatabase({ name });
return { success: true };
}
// components/ContactForm.tsx
'use client';
import { submitForm } from '@/app/actions';
export default function ContactForm() {
return (
<form action={submitForm}>
<input name="name" placeholder="お名前" />
<button type="submit">送信</button>
</form>
);
}
まとめ
Next.js 13以降で導入されたServer ComponentsとClient Componentsを適切に使い分けることで、パフォーマンスとユーザーエクスペリエンスを大幅に向上させることができます。
- Server Components: データ取得、静的コンテンツ、SEO重視のページ
- Client Components: ユーザーインタラクション、状態管理、ブラウザAPI
デフォルトはServer Componentなので、インタラクティブな機能が必要な部分だけを'use client'でClient Componentにする設計が推奨されます。