コンポーネントを理解する
この章の目次開く
Next.js(App Router)では、Reactコンポーネントが2種類に分かれます。Server Components と Client 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>
);
}注目すべきは、コンポーネント関数に 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>
);
}これをClient Componentでやろうとすると、ブラウザ上でDBクライアントを動かすことになり、接続情報や認証情報を守れません。DB操作は、Server Component、Route Handlers、Server 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>;
}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>;
}クライアントに送る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>
);
}このページ全体を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>
);
}Client Componentsが必要になるのは、主に以下のケースです:
useState、useEffectなどのReact Hooksを使うonClick、onChangeなどのイベントハンドラを使う- ブラウザ専用のAPI(
window、localStorageなど)にアクセスする
使い分けの指針
Server ComponentsとClient Componentsをどう使い分けるかは、まず「ブラウザ上で動く必要があるか?」から考えると分かりやすいです。
迷ったら、まずServer Componentで作ってみます。useState、onClick、window などが必要になった部分だけClient Componentに切り出す、という順番が実務では扱いやすいです。
表で整理すると、次のようになります。
| やりたいこと | Server | Client |
|---|---|---|
| データの取得 | 得意 | 可能 |
| 機密情報へのアクセス | 得意 | 不可 |
| ステート管理 | 不可 | 得意 |
| イベントハンドラ | 不可 | 得意 |
| ブラウザ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>
);
}// 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>
);
}このように、ページ全体は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 }}
/>
);
}まとめ
- App Routerではデフォルトで Server Components
"use client"を付けると Client Components になる- サーバーで完結する処理はServer Componentsで、UIのインタラクションはClient Componentsで
- 両者を適切に組み合わせることで、パフォーマンスと開発体験を両立できる
次の章では、ページとURLを対応づける ルーティングとページの作り方 へ進みます。フォルダを作るだけで画面が増えていく、App Routerの中核を体験しましょう。
