AIツクリラボ
Next.js制作Tips7分で読めます🔵 実装記録

Reactのサーバーコンポーネント・クライアントコンポーネントの使い分け

App Routerを触り始めると必ずぶつかるのが、「サーバーコンポーネント」と「クライアントコンポーネント」という2つの概念です。最初は違いがピンと来ませんでしたが、実際に手を動かしながら整理できたので、このブログでの実例を交えて説明します。

🛠 実装してみて分かったこと

一番混乱したのは、「デフォルトがどっちなのか」という点でした。App Routerではコンポーネントはデフォルトでサーバーコンポーネントになっていて、`"use client"`と明示的に書いたものだけがクライアントコンポーネントになります。この前提を知らずに書き始めると、「なぜこのコードはブラウザで動かないんだ」というエラーに何度も遭遇しました。結局、「本当にブラウザ側の状態(クリックやlocalStorageなど)が必要な部分だけ`"use client"`を付ける」というシンプルなルールに落ち着いてからは、迷わなくなりました。

サーバーコンポーネントとクライアントコンポーネントの違い

観点サーバーコンポーネントクライアントコンポーネント
実行される場所サーバー側(ビルド時・リクエスト時)ブラウザ側
"use client"の要否不要(デフォルト)必要
useState・useEffectの使用使えない使える
ファイルの読み込みなど直接書ける(このブログのMarkdown読み込みなど)できない
JavaScriptの配信量ブラウザに送られないブラウザに送られる

このブログでの実例

実際の記事ページ(BlogPostPage)を図にすると、こうなります。ページのほとんどはサーバーコンポーネントのままで、ブラウザの状態(localStorage・クリック)が必要なThemeToggleだけをクライアントコンポーネントとして切り出しています。

BlogPostPageというサーバーコンポーネントの中に、Markdown読み込み・記事本文の描画・JSON-LDの埋め込みといったサーバー側の処理が並び、"use client"を付けた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モジュールでファイルを読み込む処理も、サーバーコンポーネントだからこそ違和感なく書けています。

なぜparamsawaitする必要があるのか

Next.js 15でApp RouterのparamsがPromiseを返す仕様に変わったため、以前のバージョンの感覚でparams.slugと直接プロパティを読もうとすると、値が取れず動きません。バージョンアップ時に見落としやすい変更点で、このブログでも最初はこの仕様を知らずに書いてしまい、型エラーで気づきました。

try/catchisPublishedのチェックは、存在しないslugへのアクセスや、予約投稿中の記事へ日付前に直接URLでアクセスされた場合に備えたものです。どちらのケースもnotFound()を呼んで404を返し、未公開の内容が見えてしまわないようにしています。

クライアントコンポーネントにしている部分

一方、テーマ切り替えボタンはクリックイベントとlocalStorageを扱うため、"use client"を付けたクライアントコンポーネントにしています。

"use client";
import { useSyncExternalStore } from "react";
 
export default function ThemeToggle() {
  // クリックやlocalStorageなど、ブラウザ側の機能を使う処理
}

使い分けの判断基準

実際に書いていて固まった判断基準はシンプルです。

  1. まずサーバーコンポーネントとして書き始める
  2. useStateuseEffect・クリックハンドラなどが必要になった時点で、そのコンポーネントだけ"use client"を付ける
  3. "use client"を付けたコンポーネントは、できるだけ小さく切り出す(親のページ全体をクライアントコンポーネントにすると、配信されるJavaScriptが不必要に増えてしまうため)

3番目が特に大事で、最初はページ全体に"use client"を付けてしまい、後から「本当にクライアント側の処理が必要な部分」だけを切り出す、という手戻りを経験しました。

よくある質問

Q. サーバーコンポーネントの中にクライアントコンポーネントを置けますか? A. できます。むしろそれが基本の構成で、ページ全体はサーバーコンポーネントのまま、ボタンなど一部だけをクライアントコンポーネントとして埋め込みます。

Q. クライアントコンポーネントの中にサーバーコンポーネントを置けますか? A. 直接は置けません。クライアントコンポーネントに渡すchildrenとして、外側(サーバーコンポーネント側)で用意しておく必要があります。

Q. とりあえず全部"use client"にしてしまうのはダメですか? A. 動作はしますが、Next.jsの静的生成やサーバーコンポーネントのメリット(JavaScript配信量の削減など)を活かせなくなります。必要な部分だけに絞るのがおすすめです。

まとめ

サーバーコンポーネントとクライアントコンポーネントの使い分けは、「まずサーバーコンポーネントで書き、ブラウザ側の機能が必要になった部分だけクライアントコンポーネントに切り出す」というシンプルな考え方で十分対応できます。難しく考えすぎず、必要になってから対応する、くらいの気持ちで始めてよいと思います。

この記事を書いた人

ラボ管理人

AIツール・ノーコード・Next.jsで実際に手を動かしながら、AIツクリラボを運営しています。

運営者情報を見る →