Reactのサーバーコンポーネント・クライアントコンポーネントの使い分け
App Routerを触り始めると必ずぶつかるのが、「サーバーコンポーネント」と「クライアントコンポーネント」という2つの概念です。最初は違いがピンと来ませんでしたが、実際に手を動かしながら整理できたので、このブログでの実例を交えて説明します。
🛠 実装してみて分かったこと
一番混乱したのは、「デフォルトがどっちなのか」という点でした。App Routerではコンポーネントはデフォルトでサーバーコンポーネントになっていて、`"use client"`と明示的に書いたものだけがクライアントコンポーネントになります。この前提を知らずに書き始めると、「なぜこのコードはブラウザで動かないんだ」というエラーに何度も遭遇しました。結局、「本当にブラウザ側の状態(クリックやlocalStorageなど)が必要な部分だけ`"use client"`を付ける」というシンプルなルールに落ち着いてからは、迷わなくなりました。
サーバーコンポーネントとクライアントコンポーネントの違い
| 観点 | サーバーコンポーネント | クライアントコンポーネント |
|---|---|---|
| 実行される場所 | サーバー側(ビルド時・リクエスト時) | ブラウザ側 |
"use client"の要否 | 不要(デフォルト) | 必要 |
| useState・useEffectの使用 | 使えない | 使える |
| ファイルの読み込みなど | 直接書ける(このブログのMarkdown読み込みなど) | できない |
| JavaScriptの配信量 | ブラウザに送られない | ブラウザに送られる |
このブログでの実例
実際の記事ページ(BlogPostPage)を図にすると、こうなります。ページのほとんどはサーバーコンポーネントのままで、ブラウザの状態(localStorage・クリック)が必要なThemeToggleだけをクライアントコンポーネントとして切り出しています。
サーバーコンポーネントのまま書いている部分
記事一覧や記事詳細のページは、Markdownファイルを読み込んで表示するだけなので、サーバーコンポーネントのままで十分です。実際の記事詳細ページ(blog/[slug]/page.tsx)は次のようになっています。
type Props = {
params: Promise<{ slug: string }>;
};
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
let post;
try {
post = await getPostBySlug(slug);
} catch {
notFound();
}
if (!isPublished(post)) {
notFound();
}
return <article>{/* ... */}</article>;
}fsモジュールでファイルを読み込む処理も、サーバーコンポーネントだからこそ違和感なく書けています。
なぜparamsをawaitする必要があるのか
Next.js 15でApp RouterのparamsがPromiseを返す仕様に変わったため、以前のバージョンの感覚でparams.slugと直接プロパティを読もうとすると、値が取れず動きません。バージョンアップ時に見落としやすい変更点で、このブログでも最初はこの仕様を知らずに書いてしまい、型エラーで気づきました。
try/catchとisPublishedのチェックは、存在しないslugへのアクセスや、予約投稿中の記事へ日付前に直接URLでアクセスされた場合に備えたものです。どちらのケースもnotFound()を呼んで404を返し、未公開の内容が見えてしまわないようにしています。
クライアントコンポーネントにしている部分
一方、テーマ切り替えボタンはクリックイベントとlocalStorageを扱うため、"use client"を付けたクライアントコンポーネントにしています。
"use client";
import { useSyncExternalStore } from "react";
export default function ThemeToggle() {
// クリックやlocalStorageなど、ブラウザ側の機能を使う処理
}使い分けの判断基準
実際に書いていて固まった判断基準はシンプルです。
- まずサーバーコンポーネントとして書き始める
useState・useEffect・クリックハンドラなどが必要になった時点で、そのコンポーネントだけ"use client"を付ける"use client"を付けたコンポーネントは、できるだけ小さく切り出す(親のページ全体をクライアントコンポーネントにすると、配信されるJavaScriptが不必要に増えてしまうため)
3番目が特に大事で、最初はページ全体に"use client"を付けてしまい、後から「本当にクライアント側の処理が必要な部分」だけを切り出す、という手戻りを経験しました。
よくある質問
Q. サーバーコンポーネントの中にクライアントコンポーネントを置けますか? A. できます。むしろそれが基本の構成で、ページ全体はサーバーコンポーネントのまま、ボタンなど一部だけをクライアントコンポーネントとして埋め込みます。
Q. クライアントコンポーネントの中にサーバーコンポーネントを置けますか?
A. 直接は置けません。クライアントコンポーネントに渡すchildrenとして、外側(サーバーコンポーネント側)で用意しておく必要があります。
Q. とりあえず全部"use client"にしてしまうのはダメですか?
A. 動作はしますが、Next.jsの静的生成やサーバーコンポーネントのメリット(JavaScript配信量の削減など)を活かせなくなります。必要な部分だけに絞るのがおすすめです。
まとめ
サーバーコンポーネントとクライアントコンポーネントの使い分けは、「まずサーバーコンポーネントで書き、ブラウザ側の機能が必要になった部分だけクライアントコンポーネントに切り出す」というシンプルな考え方で十分対応できます。難しく考えすぎず、必要になってから対応する、くらいの気持ちで始めてよいと思います。