Next.js App Router の Server Components と Client Components を完全に整理する
Next.js の App Router を使い始めたとき、「どっちを使えばいいかわからない」という混乱、あると思います。
"use client" を書けばいいのはわかった。でも書かないとデフォルトで何になるの?useEffect が使えないのはなぜ?データフェッチはどこでやればいい?
この記事では、Server Components と Client Components の違いを整理して、「何をどこに書けばいいか」の判断基準を明確にします。
この記事で学べること:
- Server Components と Client Components の根本的な違い
- それぞれができること・できないこと
- データフェッチのパターン
- コンポーネントの使い分け判断フロー
- よくある間違いとその解決策
検証環境: Next.js 14+(App Router)
根本的な違いを一言で
- Server Components: サーバーでレンダリングされる。ブラウザに届くのは HTML のみ
- Client Components: ブラウザでもレンダリングされる。JavaScript がブラウザに届く
App Router ではデフォルトが Server Components です。"use client" を書いたファイルが Client Components になります。
Server Components でできること・できないこと
できること
// app/users/page.tsx(Server Component、"use client" なし)
async function getUsers() {
// サーバーサイドで直接DBアクセスやAPIコール可能
const res = await fetch("https://api.example.com/users", {
next: { revalidate: 60 }, // 60秒キャッシュ
});
return res.json();
}
export default async function UsersPage() {
// async/await で直接データフェッチできる
const users = await getUsers();
return (
<ul>
{users.map((user: { id: number; name: string }) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Server Components では:
-
async/awaitでデータフェッチできる(useEffect 不要) - DBに直接アクセスできる(Prisma, Drizzle 等)
- 環境変数(
NEXT_PUBLIC_なしのもの)を使える -
fs(ファイルシステム)等のサーバー専用モジュールを使える - バンドルサイズに影響しない(JSがブラウザに届かない)
できないこと
// ❌ Server Components でこれらは使えない
"use server"; // これは Server Actions 用、Server Components の宣言ではない
import { useState, useEffect, useRef } from "react"; // ❌ Hooks 使用不可
import { useRouter } from "next/navigation"; // ❌ クライアント専用
export default function BadServerComponent() {
const [count, setCount] = useState(0); // ❌ エラー
useEffect(() => { // ❌ エラー
console.log("mounted");
}, []);
return <button onClick={() => setCount(c => c + 1)}>❌</button>; // ❌ イベントハンドラ不可
}
Server Components では React Hooks・イベントハンドラ・ブラウザ専用 API(window, document 等)は使えません。
Client Components でできること・できないこと
// app/components/Counter.tsx
"use client"; // この宣言でClient Componentになる
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0); // ✅ Hooks 使用可能
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button> {/* ✅ イベントハンドラ可能 */}
</div>
);
}
Client Components では:
-
useState/useEffect/useRefなどの Hooks が使える -
onClickなどのイベントハンドラが使える -
window/documentなどブラウザ専用 API が使える -
useRouter/usePathnameなどのクライアント専用フックが使える
ただし、 JavaScript バンドルがブラウザに届く ため、重いライブラリを Client Components でインポートするとバンドルサイズが大きくなります。
コンポジションパターン ─ Server の中に Client を置く
重要なのが、 Server Components の中に Client Components を置ける ということです。
// app/dashboard/page.tsx(Server Component)
import UserList from "./UserList"; // Server Component
import SearchBar from "../components/SearchBar"; // Client Component
import StatsChart from "../components/StatsChart"; // Client Component
async function getDashboardData() {
const res = await fetch("https://api.example.com/dashboard");
return res.json();
}
export default async function DashboardPage() {
const data = await getDashboardData(); // サーバーでデータフェッチ
return (
<div>
<SearchBar /> {/* Client Component: 検索入力をインタラクティブに */}
<UserList users={data.users} /> {/* Server Component: 表示のみ */}
<StatsChart data={data.stats} /> {/* Client Component: グラフのインタラクション */}
</div>
);
}
ページ全体を Client Component にするのではなく、 インタラクションが必要な部分だけ を Client Component にするのが基本方針です。
Client Components の中に Server Components を置けない
// ❌ これはできない
"use client";
import ServerComponent from "./ServerComponent"; // Server Componentを直接import
export default function ClientParent() {
return (
<div>
<ServerComponent /> {/* ❌ Client Componentの子にServer Componentは置けない */}
</div>
);
}
// ✅ children として渡す(propsとして渡す)ならOK
"use client";
export default function ClientParent({ children }: { children: React.ReactNode }) {
return <div>{children}</div>; // ✅ childrenがServer Componentでも問題なし
}
// 親のServer Componentで:
// <ClientParent>
// <ServerComponent /> {/* ✅ これはOK */}
// </ClientParent>
children として渡す場合は、Server Component を Client Component の子として使えます。
データフェッチのパターン
パターン1: ページレベルでフェッチ(推奨)
// app/posts/page.tsx(Server Component)
async function getPosts() {
const res = await fetch("https://api.example.com/posts");
if (!res.ok) throw new Error("投稿の取得に失敗しました");
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return <PostList posts={posts} />;
}
パターン2: 並列フェッチで待ち時間を減らす
// 複数のデータを並列で取得
export default async function ProfilePage({ params }: { params: { id: string } }) {
// Promise.all で並列実行
const [user, posts, followers] = await Promise.all([
fetchUser(params.id),
fetchPosts(params.id),
fetchFollowers(params.id),
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<FollowerCount count={followers.length} />
</div>
);
}
パターン3: Suspense で段階的に表示する
import { Suspense } from "react";
export default function Page() {
return (
<div>
<h1>ダッシュボード</h1>
{/* 遅いコンポーネントを Suspense で囲む */}
<Suspense fallback={<p>読み込み中...</p>}>
<SlowDataComponent /> {/* 遅いデータを持つServer Component */}
</Suspense>
<FastComponent /> {/* すぐ表示されるコンポーネント */}
</div>
);
}
使い分けの判断フロー
迷ったときの判断基準をシンプルにまとめると:
useState / useEffect / イベントハンドラ が必要?
↓ Yes → "use client" をつける(Client Component)
↓ No → Server Component のまま(デフォルト)
もう少し具体的に:
| 使う場面 | どちらを使う |
|---|---|
| データフェッチ(DB・API) | Server Component |
| フォーム送信(状態管理不要) | Server Component + Server Actions |
| インタラクティブなUI(カウンター・トグル等) | Client Component |
| ルート変更(useRouter) | Client Component |
| ブラウザAPIの使用(localStorage等) | Client Component |
| 重いライブラリの使用 | Server Component(可能なら) |
よくある間違いと解決策
間違い1: とりあえず全部 "use client" にする
// ❌ 全ページに "use client" をつける
"use client";
export default async function Page() { // async は Client Component では使えない(実質意味なし)
const data = await fetch("..."); // これはサーバーサイドにならない
...
}
Client Component にすると、データフェッチが useEffect 経由になり、初期表示が遅くなります。また、サーバー専用の API(DB接続等)が使えなくなります。
間違い2: Client Component でサーバーの秘密情報を使う
// ❌ Client Component でAPIキーを直接使う
"use client";
const SECRET_KEY = process.env.SECRET_API_KEY; // ブラウザに漏れる可能性
export default function DangerousComponent() {
// SECRET_KEY がブラウザのJavaScriptに含まれてしまう
}
NEXT_PUBLIC_ プレフィックスのない環境変数はサーバーサイド専用ですが、Client Component でアクセスしようとすると undefined になります。秘密情報は Server Component か Server Actions で扱います。
間違い3: Client Component でデータフェッチを useEffect で行う
// ❌ よくある古いパターン
"use client";
import { useState, useEffect } from "react";
export default function DataPage() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/data").then(r => r.json()).then(setData);
}, []);
if (!data) return <p>読み込み中...</p>;
return <div>{JSON.stringify(data)}</div>;
}
// ✅ Server Component で直接フェッチ
export default async function DataPage() {
const data = await fetch("https://api.example.com/data").then(r => r.json());
return <div>{JSON.stringify(data)}</div>;
}
Server Component で直接フェッチすれば、ローディング状態の管理も不要になり、コードがシンプルになります。
まとめ
App Router の基本方針は 「デフォルトは Server Component、インタラクションが必要な部分だけ Client Component」 です。
- Server Component: データフェッチ・DB接続・重い処理はここ
-
Client Component:
"use client"+ Hooks/イベントハンドラが必要な部分だけ - コンポジション: Server の中に Client を入れる。逆は children 経由で
全部 "use client" にしてしまうと、App Router の恩恵(サーバーサイドレンダリング・キャッシュ・バンドルサイズ削減)が受けられなくなります。「インタラクションが必要かどうか」を基準に判断すると、迷う場面が減る気がします。