React Server Components完全ガイド|クライアントコンポーネントとの違いと判断基準を徹底解説

プログラミング

React Server Componentsの学習を始めたものの、クライアントコンポーネントとどう使い分けたらいいのか判断できず、プロジェクトで実装するタイミングに迷っていませんか。

Next.jsの普及によってReact Server Components(以下RSC)への関心が急速に高まっていますが、その本質的な違いや使い分けの基準を正確に理解しているエンジニアは意外と少ないのが実情です。

本記事では、React Server Componentsの動作原理から、クライアントコンポーネントとの違い、そして実務での選択基準まで、網羅的かつ実践的に解説します。記事を読み終わる頃には、プロジェクトで自信を持ってRSCを活用できるようになるでしょう。

React Server Componentsとは何か

React Server Components(RSC)は、React 18で導入されたコンポーネント処理モデルです。従来のReactコンポーネントがブラウザ側で動作するのに対して、RSCはサーバー側で実行され、レンダリング結果だけがクライアントに送信されます。

具体的には、RSCはサーバー上で完全にレンダリングされ、JavaScriptバンドルとしてクライアントに送信されません。これにより、ペイロードサイズの削減とセキュリティの向上が実現します。

Next.jsのappディレクトリが採用されて以来、RSCはモダンReactアプリケーション開発のデフォルト選択肢となっています。既存のクライアントレンダリング主体の開発から、サーバー・クライアントのハイブリッド型へのパラダイムシフトを意味しています。

Server ComponentsとClient Componentsの違い

Server ComponentsとClient Componentsを比較することで、それぞれの役割が明確になります。以下の表を参照してください。

項目 Server Component Client Component
実行環境 サーバー側のみ ブラウザ側のみ
HTMLの送信 レンダリング済みHTML JavaScriptコード
データベースアクセス 直接可能 API経由で必要
環境変数使用 全て使用可能 NEXT_PUBLIC_接頭辞のみ
インタラクティビティ 不可 可能(useState等)
バンドルサイズ クライアント側に含まれない JavaScriptとして送信
useEffect/useCallback 使用不可 使用可能
ユースケース データ取得・表示が主 ユーザー操作が必要

この表から明らかなように、Server ComponentsとClient Componentsは異なる目的を持っており、単純にどちらかを選ぶのではなく、コンポーネントの責務に応じて使い分ける必要があります。

React Server Componentsの使い方

基本的なServer Componentの書き方

Next.jsのappディレクトリにおいて、デフォルトではすべてのコンポーネントがServer Componentsとして動作します。Server Componentでは、直接データベースクエリを実行したり、APIサーバーへのアクセスを行ったりできます。

// app/posts/page.tsx(Server Component)
import { db } from '@/lib/db';

export default async function PostsPage() {
  // サーバー側で直接データを取得
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div>
      <h1>ブログ投稿一覧</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

このコード例では、async関数として宣言されたServer Componentが、サーバー側で直接データベースクエリを実行しています。ブラウザには完全にレンダリングされたHTMLが送信されるため、JSONペイロードは最小限に抑えられます。

Client Componentの明示的な指定

インタラクティビティが必要な場合は、ファイルの最上部に'use client'ディレクティブを記述してClient Componentとして明示的に指定します。

// app/components/PostComments.tsx
'use client';

import { useState } from 'react';

export default function PostComments({ postId }: { postId: string }) {
  const [comments, setComments] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsLoading(true);
    
    const formData = new FormData(e.currentTarget);
    const text = formData.get('text') as string;

    try {
      const response = await fetch(`/api/comments`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ postId, text }),
      });
      
      const newComment = await response.json();
      setComments([...comments, newComment]);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea name="text" placeholder="コメントを入力..." required />
      <button type="submit" disabled={isLoading}>
        {isLoading ? '送信中...' : 'コメントを送信'}
      </button>
    </form>
  );
}

'use client'ディレクティブにより、このコンポーネントはブラウザ側でのみ実行され、useStateuseEffectなどのフックが使用可能になります。

Server ComponentとClient Componentの組み合わせ

実務では、Server ComponentとClient Componentを組み合わせて使用します。Server Component側でデータを取得し、Client Component側でインタラクティビティを提供するパターンが一般的です。

// app/posts/[id]/page.tsx(Server Component)
import { db } from '@/lib/db';
import PostComments from '@/components/PostComments';
import LikeButton from '@/components/LikeButton';

export default async function PostDetailPage({
  params,
}: {
  params: { id: string };
}) {
  const post = await db.post.findUnique({
    where: { id: params.id },
    include: { author: true },
  });

  if (!post) {
    return <div>投稿が見つかりません</div>;
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>著者: {post.author.name}</p>
      <div>{post.content}</div>
      
      {/* Client Componentをここに組み込む */}
      <LikeButton postId={post.id} initialLikes={post.likes} />
      <PostComments postId={post.id} />
    </article>
  );
}

このパターンでは、Server Componentがデータを効率的に取得し、Client Componentがユーザーインタラクションをハンドルするため、最適なパフォーマンスとUXが実現します。

Server Componentとの判断基準

Server Componentを選択すべき場合

以下のいずれかに該当する場合は、Server Componentの使用を優先してください。

  • データベースに直接アクセスする必要がある場合
  • 認証トークンやAPI キーなどの機密情報を使用する場合
  • 大規模なライブラリをバンドルサイズに含めたくない場合
  • ユーザー操作を必要としないコンポーネント(単純な表示のみ)である場合
  • バックエンド層のロジックをコンポーネント内で実行したい場合

Server Componentはバンドルサイズを削減し、セキュリティを向上させるため、デフォルトの選択肢として考えるべきです。

Client Componentを選択すべき場合

以下の場合は、Client Componentの使用が必須です。

  • useStateやuseReducerなどの状態管理が必要な場合
  • useEffectやuseCallbackなどのライフサイクルフックを使用する場合
  • クリック・フォーム送信などのユーザー操作をハンドルする場合
  • ブラウザAPIに依存する機能(localStorage、geolocation等)を使用する場合
  • リアルタイム更新や自動更新が必要な場合

Client Componentが必要な場面は実は限定的です。多くの開発者は従来のReact開発に慣れているため、無意識にClient Componentを多用しがちですが、意識的にServer Componentの使用を検討することが重要です。

判断基準の決定木

コンポーネントの種類を判断する際は、以下のフローに従うことをお勧めします。

  1. ユーザーインタラクションが必要か?必要であればClient Componentにする
  2. ブラウザAPIや状態管理が必要か?必要であればClient Componentにする
  3. データベースアクセスやAPI キーが必要か?必要であればServer Componentにする
  4. 上記いずれにも該当しない場合は、Server Componentとしてバンドルサイズ削減を優先する

このフローに従うことで、組織全体での一貫した判断が可能になり、コードレビューも効率化されます。

React Server Componentsの実装上の注意点

パフォーマンス最適化のポイント

Server Componentはパフォーマンス向上の可能性を秘めていますが、いくつかの注意点があります。まず、Server Component内での重い処理(たとえば大量のデータベースクエリ)はウォーターフォール問題を引き起こす可能性があります。複数のデータベースアクセスが必要な場合は、並列処理やバッチ処理を検討してください。

// 悪い例:ウォーターフォール
export default async function Dashboard() {
  const user = await db.user.findUnique({ where: { id: userId } });
  const posts = await db.post.findMany({ where: { authorId: user.id } });
  const comments = await db.comment.findMany({ where: { authorId: user.id } });
  // 各クエリが順次実行されるため、遅い

  return <div>...</div>;
}

// 良い例:並列処理
export default async function Dashboard() {
  const user = await db.user.findUnique({ where: { id: userId } });
  
  const [posts, comments] = await Promise.all([
    db.post.findMany({ where: { authorId: user.id } }),
    db.comment.findMany({ where: { authorId: user.id } }),
  ]);

  return <div>...</div>;
}

このように、複数の非同期処理をPromise.all()で並列実行することで、全体の応答時間を短縮できます。

Streaming と Suspenseの活用

Next.jsのStreaming機能とReactのSuspenseを組み合わせることで、遅いコンポーネントをバッファリングせず、段階的にUIを表示できます。

// app/dashboard/page.tsx
import { Suspense } from 'react';
import SlowComponent from '@/components/SlowComponent';
import LoadingFallback from '@/components/LoadingFallback';

export default function DashboardPage() {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <Suspense fallback={<LoadingFallback />}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

Suspenseを使用することで、SlowComponentの読み込み中にLoadingFallbackが表示され、ページ全体がブロックされません。これにより、ユーザー体験が大幅に向上します。

組織的な導入のコツ

Team全体でServer Componentsを採用する際は、ESlintルールの設定が有効です。@next/eslint-plugin-nextを使用して、'use client'の過度な使用を検出し、チーム内での一貫性を確保できます。

AIツールで業務効率化を実現したエンジニア5人の実例|ChatGPT導入で月20時間削減の事例もの記事でも紹介されているように、ChatGPTやCursorエディタなどのAIツールを活用することで、Server ComponentsとClient Componentsの適切な分離パターンを学習させることができます。

実装パターン集

フォーム送信パターン

フォーム送信は実務で頻出するパターンです。Server ActionとClient Componentを組み合わせて実装します。

// app/actions.ts(Server Side)
'use server';

import { db } from '@/lib/db';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  const post = await db.post.create({
    data: { title, content, authorId: 'user-id' },
  });

  return post;
}

// app/components/PostForm.tsx(Client Side)
'use client';

import { createPost } from '@/app/actions';
import { useState } from 'react';

export default function PostForm() {
  const [isPending, setIsPending] = useState(false);

  async function handleSubmit(formData: FormData) {
    setIsPending(true);
    try {
      const post = await createPost(formData);
      alert(`投稿ID ${post.id} が作成されました`);
    } finally {
      setIsPending(false);
    }
  }

  return (
    <form action={handleSubmit}>
      <input name="title" placeholder="タイトル" required />
      <textarea name="content" placeholder="内容" required />
      <button type="submit" disabled={isPending}>
        {isPending ? '送信中...' : '投稿'}
      </button>
    </form>
  );
}

このパターンでは、Server Action側で安全にデータベースアクセスを行い、Client Component側でローディング状態のUIハンドリングを行う、理想的な役割分担が実現しています。

検索・フィルタリングパターン

検索フィルタリング機能は、Server ComponentとClient Componentを組み合わせた代表的なパターンです。URL検索パラメータを活用することで、ブックマークや共有が容易になります。

// app/products/page.tsx
export default async function ProductsPage({
  searchParams,
}: {
  searchParams: { category?: string; sort?: string };
}) {
  const category = searchParams.category || 'all';
  const sort = searchParams.sort || 'popular';

  const products = await db.product.findMany({
    where: category !== 'all' ? { category } : {},
    orderBy: sort === 'price' ? { price: 'asc' } : { views: 'desc' },
  });

  return (
    <div>
      <ProductFilter category={category} sort={sort} />
      <ProductGrid products={products} />
    </div>
  );
}

// app/components/ProductFilter.tsx
'use client';

import { useRouter } from 'next/navigation';

export default function ProductFilter({
  category,
  sort,
}: {
  category: string;
  sort: string;
}) {
  const router = useRouter();

  const handleFilterChange = (newCategory: string) => {
    router.push(`/products?category=${newCategory}&sort=${sort}`);
  };

  return (
    <select value={category} onChange={(e) => handleFilterChange(e.target.value)}>
      <option value="all">すべてのカテゴリー</option>
      <option value="electronics">電子機器</option>
      <option value="books">書籍</option>
    </select>
  );
}

このパターンでは、フィルター値がURLパラメータとして保持されるため、ページのリロードやブックマークが正確に機能します。

React Server Components導入時のよくある落とし穴

Server Componentsの導入で多くの開発者が陥る罠があります。一つ目は、Client Componentの多用です。従来のReact開発の習慣で、すべてのコンポーネントに'use client'を付与してしまうケースが散見されます。これはServer Componentsのメリットを完全に失わせてしまいます。

二つ目は、Server Component内でクライアント専用フックの使用を試みる誤りです。useStateuseEffectはServer Componentでは使用できないため、コンパイルエラーが発生します。

三つ目は、パフォーマンスの過度な最適化です。Server Componentはすでに高速ですが、さらに細粒度の最適化を試みると、複雑性が増し、保守性が低下します。

Cursorエディタ完全ガイド|AI活用で開発速度3倍へ。エンジニアが実際に使ったリアルレビューの記事で紹介されているように、AIアシスタント機能を活用することで、これらの落とし穴を事前に防ぐことができます。

おすすめ書籍・ガジェット

まとめ

React Server Componentsは、モダンウェブアプリケーション開発における重要なパラダイムシフトです。バンドルサイズの削減、セキュリティの向上、そして開発者体験の改善をもたらします。

本記事で解説した判断基準に従うことで、プロジェクトごとに最適なコンポーネント設計を実現できます。Server Componentをデフォルトとして、必要な場面のみClient Componentを選択する思考習慣を身につけることが成功の鍵です。

初回導入時は学習コストがかかりますが、長期的には開発効率とアプリケーション性能の両面で大きなメリットが得られます。Perplexity AIの使い方|エンジニアが技術調査を3倍速くする実践チュートリアルなどのリソースを活用しながら、チーム全体で知識を深めていくことをお勧めします。

React Server ComponentsはNext.js以外で使用できますか?

React Server Componentsはレイアウトラッパーやバンドラーの特別なサポートが必要なため、Next.js(13以降)が最も安定した実装環境です。ただし、Remix、Hydrogen、Wunderpackなど、RSCをサポートする他のフレームワークも存在します。ただし、実務的にはNext.jsの採用が最も安全かつ情報が豊富です。

既存プロジェクトをServer Componentsに移行するべきですか?

既存のpagesディレクトリベースのNext.jsプロジェクトからappディレクトリへの段階的な移行が推奨されます。すべてを一度に変更する必要はなく、新機能の追加時からappディレクトリを使用開始し、徐々に既存機能を移行するアプローチが現実的です。プロジェクト規模が小さい場合は全面移行も検討できます。

Server ComponentでAPIキーを安全に使用するには?

Server Componentでは、環境変数ファイル(.env.local)に機密情報を保存できます。NEXT_PUBLIC_接頭辞のない環境変数はサーバー側のみでアクセス可能で、クライアント側には送信されません。このため、Server Componentはデータベース接続文字列やAPIキーを直接含めても安全です。ただし、Server Action経由で機密情報を扱う場合は、さらなるセキュリティ考慮が必要です。

Server ComponentのデータをClient Componentで再フェッチするにはどうすべきですか?

Server ComponentのデータをClient Component内で更新する場合は、APIエンドポイントを作成し、Client Component側のuseEffectまたはuseCallbackからフェッチするパターンが標準です。React Query や SWRなどのデータフェッチングライブラリを活用することで、キャッシング、再検証、エラーハンドリングが簡潔に記述できます。Server Actionsを使用したアプローチもあり、どちらを選択するかはプロジェクトの設計思想によります。

タイトルとURLをコピーしました