0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js App Router の Server Components と Client Components を完全に整理する

0
Posted at

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 の恩恵(サーバーサイドレンダリング・キャッシュ・バンドルサイズ削減)が受けられなくなります。「インタラクションが必要かどうか」を基準に判断すると、迷う場面が減る気がします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?