ウェブエンジニア問題集
第3章

コンポーネントを理解する

8
この章の目次開く

Next.js(App Router)では、Reactコンポーネントが2種類に分かれます。Server ComponentsClient Components です。この章では、その違いと使い分けを理解しましょう。

学習者学習者

Server Components と Client Components…?Reactのコンポーネントって1種類じゃないの?

Server Components

App Routerでは、すべてのコンポーネントがデフォルトでServer Componentsです。サーバー側でレンダリングされるため、以下の特徴があります:

  • データベースやファイルシステムに直接アクセスできる
  • APIキーなどの機密情報を安全に扱える
  • クライアントに送信されるJavaScriptが減る

この3つは言葉だけだと少し抽象的です。実務のコードに置き換えると、次のような意味になります。

特徴実装でいうと
データベースに直接アクセスできるdb.user.findMany() のようなDBクライアントをServer Component内で呼べる
ファイルシステムにアクセスできるfs.readFile() でMarkdownやJSONファイルを読み込める
APIキーを安全に扱えるprocess.env.SECRET_API_KEY を使って外部APIを呼べる
JavaScriptが減るただ表示するだけの一覧・記事・プロフィールをClient Componentにしなくてよい
// app/users/page.tsx — Server Component(デフォルト)
async function getUsers() {
  const res = await fetch("https://api.example.com/users");
  return res.json();
}
 
export default async function UsersPage() {
  const users = await getUsers();
 
  return (
    <div>
      <h1>ユーザー一覧</h1>
      <ul>
        {users.map((user: { id: number; name: string }) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}
tsx

注目すべきは、コンポーネント関数に async が使えることです。Server Componentsでは、非同期処理を直接コンポーネント内で実行できます。fetch のキャッシュや再検証の仕組みは、第7章「データ取得とキャッシュ」で詳しく扱います。

データベースに直接アクセスする例

Server Componentはサーバー側で実行されるため、PrismaやDrizzleのようなDBクライアントを直接呼べます。ブラウザにDB接続情報が送られるわけではありません。

// app/users/page.tsx — Server Component
import { db } from "@/lib/db";
 
export default async function UsersPage() {
  const users = await db.user.findMany({
    orderBy: { createdAt: "desc" },
  });
 
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
tsx

これをClient Componentでやろうとすると、ブラウザ上でDBクライアントを動かすことになり、接続情報や認証情報を守れません。DB操作は、Server Component、Route HandlersServer Actions のようなサーバー側の場所に置くのが基本です。

ファイルシステムにアクセスする例

たとえば、content/ 配下のMarkdownやJSONを読み込んでページに表示したい場合もServer Componentが向いています。

// app/docs/page.tsx — Server Component
import { readFile } from "node:fs/promises";
import path from "node:path";
 
export default async function DocsPage() {
  const filePath = path.join(process.cwd(), "content", "intro.md");
  const markdown = await readFile(filePath, "utf8");
 
  return <pre>{markdown}</pre>;
}
tsx

fs はNode.jsのAPIなので、ブラウザでは使えません。だからファイル読み込みはClient Componentではなく、サーバー側で完結させます。

APIキーを安全に使う例

外部APIを呼ぶとき、秘密のAPIキーを使うことがあります。Server Componentなら process.env から秘密情報を読み、サーバー側でAPIを呼べます。

// app/weather/page.tsx — Server Component
async function getWeather() {
  const res = await fetch("https://api.example.com/weather", {
    headers: {
      Authorization: `Bearer ${process.env.WEATHER_API_KEY}`,
    },
  });
 
  return res.json();
}
 
export default async function WeatherPage() {
  const weather = await getWeather();
 
  return <p>今日の天気: {weather.summary}</p>;
}
tsx

クライアントに送るJavaScriptが減るとは

記事本文、商品一覧、ユーザープロフィールのように「取得したデータを表示するだけ」のUIは、ブラウザで状態管理やクリック処理をする必要がありません。

// Server Componentで十分な例
export default async function ProductListPage() {
  const products = await getProducts();
 
  return (
    <div>
      {products.map((product) => (
        <article key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.price}円</p>
        </article>
      ))}
    </div>
  );
}
tsx

このページ全体をClient Componentにすると、一覧表示のためのコンポーネントコードまでブラウザに送る必要が出ます。Server Componentなら、サーバーでHTMLに近い形まで作って返せるため、ブラウザに送るJavaScriptを減らせます。

先生先生

「表示するだけ」はServer Component。「クリックで変わる」「入力に反応する」はClient Component。この感覚で分けると、実装がかなり考えやすくなるよ。

Client Components

ブラウザ側でインタラクティブな動作が必要なときは、ファイルの先頭に "use client" ディレクティブを追加します:

"use client";
 
import { useState } from "react";
 
export default function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  );
}
tsx

Client Componentsが必要になるのは、主に以下のケースです:

  • useStateuseEffect などのReact Hooksを使う
  • onClickonChange などのイベントハンドラを使う
  • ブラウザ専用のAPI(windowlocalStorageなど)にアクセスする

使い分けの指針

Server ComponentsとClient Componentsをどう使い分けるかは、まず「ブラウザ上で動く必要があるか?」から考えると分かりやすいです。

迷ったら、まずServer Componentで作ってみます。useStateonClickwindow などが必要になった部分だけClient Componentに切り出す、という順番が実務では扱いやすいです。

表で整理すると、次のようになります。

やりたいことServerClient
データの取得得意可能
機密情報へのアクセス得意不可
ステート管理不可得意
イベントハンドラ不可得意
ブラウザAPI不可得意

基本方針は「できるだけServer Componentsを使い、インタラクティブな部分だけをClient Componentsに切り出す」です。

組み合わせのパターン

実際のアプリケーションでは、Server ComponentsがClient Componentsを子として含む形で組み合わせます:

// app/dashboard/page.tsx — Server Component
import Counter from "./Counter";
 
export default async function DashboardPage() {
  const data = await fetchDashboardData();
 
  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>最終更新: {data.lastUpdated}</p>
      {/* Client Componentを埋め込む */}
      <Counter />
    </div>
  );
}
tsx
// app/dashboard/Counter.tsx — Client Component
"use client";
 
import { useState } from "react";
 
export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      クリック数: {count}
    </button>
  );
}
tsx

このように、ページ全体はServer Componentで構成し、インタラクティブな部分(ここではCounter)だけをClient Componentとして切り出すのが推奨パターンです。

生HTMLの描画

Server Componentsでは、CMSから返ってきたHTML文字列やMarkdownのパース結果をそのまま描画したい場面が出てきます。 Reactの dangerouslySetInnerHTML を使えば描画は可能ですが、XSSリスクがあるため信頼できるソースに限定する必要があります。

// Server Componentでfetchした記事HTMLを描画する例
export default async function ArticlePage() {
  const article = await fetchArticle();
 
  return (
    <article
      className="prose"
      dangerouslySetInnerHTML={{ __html: article.bodyHtml }}
    />
  );
}
tsx

まとめ

  • App Routerではデフォルトで Server Components
  • "use client" を付けると Client Components になる
  • サーバーで完結する処理はServer Componentsで、UIのインタラクションはClient Componentsで
  • 両者を適切に組み合わせることで、パフォーマンスと開発体験を両立できる

次の章では、ページとURLを対応づける ルーティングとページの作り方 へ進みます。フォルダを作るだけで画面が増えていく、App Routerの中核を体験しましょう。

Next.jsクイズに挑戦するこの章で学んだNext.jsの知識を、4択クイズでアウトプットして定着させよう