React Server Components (RSC) で変わるReact開発を徹底解説
React開発は、その誕生以来、主にクライアントサイドでのUI構築に焦点を当ててきました。しかし、ウェブアプリケーションの複雑化とパフォーマンス要求の高まりに伴い、レンダリングやデータ取得をサーバー側で行うことの重要性が増しています。このような背景から生まれたのが、React Server Components(RSC)です。
RSCは、従来のクライアントサイドレンダリング(CSR)やサーバーサイドレンダリング(SSR)といった既存の概念とは一線を画す、Reactの新しいレンダリングモデルです。これにより、パフォーマンス、バンドルサイズ、開発体験など、Reactアプリケーション開発の様々な側面が劇的に変化します。
本記事では、RSCがなぜ誕生したのか、どのような仕組みで動作するのか、そしてそれがReact開発にどのような変化をもたらすのかを、詳細かつ徹底的に解説します。
1. ウェブ開発の進化とReactの役割
Reactが登場する前、ウェブ開発の主流はマルチページアプリケーション(MPA)でした。サーバーがリクエストごとに新しいHTMLページを生成し、ブラウザはそのHTMLを表示していました。ページ遷移はフルリロードを伴い、インタラクティブ性は限定的でした。
その後、Ajax技術の普及により、ページ全体をリロードすることなくコンテンツを動的に更新する手法が広まります。そして、Reactのようなライブラリが登場し、クライアントサイドでJavaScriptを使ってリッチなユーザーインターフェースを構築するシングルページアプリケーション(SPA)の時代が到来しました。
1.1. クライアントサイドレンダリング(CSR)の台頭
Reactの初期の成功は、そのコンポーネントベースのアーキテクチャと、宣言的なUI構築手法にあります。多くのReactアプリケーションは、クライアントサイドレンダリング(CSR)を採用しています。
CSRの典型的な流れは以下の通りです。
- ユーザーがURLにアクセス。
- サーバーは最小限のHTMLファイル(通常は空のdiv要素とスクリプトタグのみ)を返す。
- ブラウザがHTMLをパースし、JavaScriptファイルをダウンロード・実行する。
- Reactがブラウザ上で実行され、APIからデータを取得したり、コンポーネントをレンダリングしたりする。
- UIがブラウザに表示され、インタラクティブになる。
このモデルは、ページ遷移が高速で、複雑なUIを効率的に構築できるという利点があります。しかし、以下のような課題も抱えています。
CSRの課題
- 初期表示速度(First Contentful Paint / Largest Contentful Paint): サーバーから返されるHTMLが最小限であるため、JavaScriptのダウンロード、パース、実行、データ取得、そしてレンダリングが全て完了するまで、ユーザーは何も表示されない白い画面を見ることになります。これは特にネットワーク環境が悪い場合や、アプリケーションの規模が大きい場合に顕著になります。
- インタラクティブになるまでの時間(Time to Interactive – TTI): UIが表示されても、Reactが完全にハイドレーション(hydrate: サーバーから送られてきたHTMLにイベントハンドラなどを付加し、クライアントサイドのReactアプリケーションとして機能させること)を完了するまで、ユーザーはUIを操作できません。大量のJavaScriptが必要な場合、TTIが遅延します。
- JavaScriptバンドルサイズの増大: アプリケーションが成長するにつれて、必要なJavaScriptの量が増加します。コンポーネントのロジック、ライブラリのコード、さらにはデータ取得のロジックまで、全てがクライアントサイドのバンドルに含まれます。これはダウンロード時間と実行時間、そしてメモリ使用量に悪影響を与えます。
- データ取得の非効率性(ウォーターフォール問題): クライアントサイドでデータ取得を行う場合、コンポーネントがレンダリングされてから初めてデータ取得が開始されることがよくあります。ネストされたコンポーネントがそれぞれデータを必要とする場合、親コンポーネントのデータ取得が終わってから子コンポーネントのデータ取得が始まる、というように、複数のデータ取得リクエストが直列に実行され、UIが表示されるまでに時間がかかる「データ取得のウォーターフォール」が発生しやすくなります。
- SEO(検索エンジン最適化): クローラーがJavaScriptを実行してコンテンツをレンダリングできるとは限らないため、初期HTMLが空に近いCSRアプリケーションは、SEOに不利になる可能性があります。
1.2. サーバーサイドレンダリング(SSR)と静的サイト生成(SSG)
これらのCSRの課題に対処するため、サーバーサイドレンダリング(SSR)や静的サイト生成(SSG)といった手法が普及しました。Next.jsやGatsbyのようなフレームワークがこれらの機能を提供し、人気を博しました。
- サーバーサイドレンダリング(SSR): サーバーがリクエスト時にReactコンポーネントをHTML文字列にレンダリングし、そのHTMLをブラウザに返します。ブラウザはすぐにコンテンツを表示できます(First Contentful Paintが高速)。その後、クライアントサイドのJavaScriptがダウンロードされ、ReactがHTMLにアタッチされてインタラクティブになります(ハイドレーション)。初期表示は高速になりますが、ブラウザに送信されるJavaScriptの量はCSRと変わらず、ハイドレーションが完了するまではTTIが遅いという課題は残ります。また、サーバーはリクエストごとにHTMLを生成するため、サーバー負荷が増加する可能性があります。
- 静的サイト生成(SSG): ビルド時に全てのページを事前にHTMLファイルとして生成します。ブラウザは静的なHTMLをダウンロードするだけで済むため、最も高速な初期表示とTTIを実現できます。しかし、ページのコンテンツがビルド時以降に変化しない静的なサイト(ブログ、ドキュメントサイトなど)に限定されます。動的なコンテンツやユーザー固有のコンテンツには不向きです。
SSRやSSGはCSRの初期表示やSEOの課題を解決しましたが、JavaScriptバンドルサイズの増大や、インタラクティブになるまでの時間の遅延といった課題は部分的にしか解決できていませんでした。特に、アプリケーションの規模が大きくなるにつれて、クライアントに送信されるJavaScriptの量は依然として大きな問題でした。
また、データ取得に関しても、SSRではサーバー側でデータを取得してからHTMLを生成しますが、コンポーネントレベルでのきめ細やかなデータ取得の最適化は、依然として工夫が必要でした。
このような背景から、「JavaScriptをクライアントに送る量を最小限に抑えつつ、高速な初期表示とインタラクティブ性を両立させ、サーバーでのデータ取得をよりシンプルに行う」という、ウェブアプリケーション開発の次のステップが求められるようになりました。ここにReact Server Components(RSC)が登場します。
2. React Server Components (RSC) とは?
React Server Componentsは、Reactコンポーネントをサーバー上でレンダリングし、その結果をクライアントに送信する新しいレンダリングモデルです。しかし、これは従来のSSRとは異なります。SSRはサーバー上でコンポーネントをHTML文字列にレンダリングしますが、RSCはコンポーネントを特別なシリアライズ可能なフォーマットにレンダリングします。このフォーマットは、クライアントサイドのReactランタイムによって解釈され、UIツリーとして構築されます。
RSCの核心的なアイデアは、コンポーネントが実行される場所を選択できるようにすることです。全てのコンポーネントがクライアントで実行される必要はありません。データ取得や静的なコンテンツ表示など、クライアントのインタラクションを必要としない部分はサーバーで実行し、インタラクティブな部分だけをクライアントで実行します。
2.1. サーバーコンポーネント vs クライアントコンポーネント vs 共有コンポーネント
RSCモデルでは、コンポーネントは主に以下の3つのタイプに分類されます。
-
サーバーコンポーネント (Server Components):
- 実行場所: サーバー上でのみ実行されます。
- 特徴:
- State (
useState
) や Effect (useEffect
) は使用できません。 - ブラウザAPI (
window
,document
など) にアクセスできません。 - イベントハンドラ (
onClick
など) を直接持つことはできません。(Server Actionを介してサーバーサイドの処理をトリガーすることは可能) - ファイルシステムやデータベースなど、サーバーサイドのリソースに直接アクセスできます。
- サーバーサイドの依存関係(例: Node.jsモジュール、データベースクライアント)をインポートしても、クライアントバンドルに含まれません。
async
コンポーネントとして定義でき、await
を使って非同期処理(主にデータ取得)を待つことができます。
- State (
- 用途: データ取得、ファイル読み込み、APIキーなどの秘密情報を含むロジック、重い計算、静的なレイアウトやコンテンツの表示。
- 利点: クライアントに送信されるJavaScript量が激減、サーバーサイドでの効率的なデータ取得、機密情報の安全な取り扱い。
-
クライアントコンポーネント (Client Components):
- 実行場所: クライアント上でのみ、または初期レンダリング時にはサーバー上で(SSRとして)実行され、その後クライアントでハイドレーションされます。最終的なインタラクティブ性はクライアントで提供されます。
- 特徴:
- 従来のReactコンポーネントとほぼ同じです。
- State (
useState
) や Effect (useEffect
) を使用できます。 - ブラウザAPI (
window
,document
など) にアクセスできます。 - イベントハンドラを持つことができます。
- コンポーネントファイルの先頭に
'use client';
ディレクティブを記述して明示的に指定します。
- 用途: ユーザーインタラクション(ボタンクリック、フォーム入力)、アニメーション、ブラウザAPIへのアクセス、State管理が必要なUI部分、クライアントサイドのライブラリを使用する部分。
- 利点: 既存のReactコードやエコシステムとの互換性、豊富なインタラクティブ性の提供。
-
共有コンポーネント (Shared Components):
- 実行場所: サーバーとクライアントの両方で実行できます。
- 特徴:
'use client';
ディレクティブを持たない.js
(または.jsx
,.ts
,.tsx
) ファイルで定義されます。- StateやEffect、ブラウザAPIは使用できません。(もし使用すると、サーバー側でレンダリングされる際にエラーになるか、クライアント側でハイドレーション時に問題を引き起こす可能性があります)
- 主にUI構造やスタイリングなど、純粋なレンダーロジックを持つコンポーネントに適しています。propsを受け取り、JSXを返すようなプレゼンテーションコンポーネントが典型的です。
- 用途: UIライブラリ、デザインシステムの一部、サーバーコンポーネントとクライアントコンポーネントの両方で利用される共通のUI要素(ボタン、カード、レイアウトコンポーネントなど)。
重要なのは、これらのコンポーネントがどのように組み合わされるかです。
- サーバーコンポーネントは、他のサーバーコンポーネントをインポート・レンダリングできます。
- サーバーコンポーネントは、クライアントコンポーネントをインポート・レンダリングできます。 (ただし、クライアントコンポーネントのJSコード自体はクライアントに送信され、サーバーコンポーネントのレンダリング結果に含まれる特別な参照を通じてクライアント側でマウントされます)。
- クライアントコンポーネントは、他のクライアントコンポーネントをインポート・レンダリングできます。
- クライアントコンポーネントは、サーバーコンポーネントをインポート・レンダリング できません。サーバーコンポーネントはサーバーでのみ実行されるため、クライアントコンポーネントがインポートしても意味がないからです。ただし、サーバーコンポーネントをpropsとして子要素として受け取ることは可能です。これは、サーバーコンポーネントのレンダリング結果をクライアントコンポーネントに「穴埋め」として渡すイメージです(例:
Layout
クライアントコンポーネントの子としてPage
サーバーコンポーネントを渡す)。
この最後の点、「クライアントコンポーネントはサーバーコンポーネントを直接インポートできないが、子として受け取ることはできる」というのが、RSCにおけるコンポーネントの親子関係とデータフローを理解する上で非常に重要です。サーバーコンポーネントは「上の方」(ルートに近い方)に配置されやすく、データ取得などを行い、その結果やUIをクライアントコンポーネントに渡す、という構造が一般的になります。
2.2. RSCのレンダリングフロー
RSCのレンダリングフローは、従来のSSRやCSRとは異なります。Next.jsのApp Routerを例に説明します。
- ユーザーがURLにアクセス。
- サーバーは、リクエストされたルートに対応するサーバーコンポーネントを特定します。
- サーバー上で、ルートのサーバーコンポーネントからレンダリングが開始されます。サーバーコンポーネント内でawaitによるデータ取得が行われます。
- レンダリングツリー中にクライアントコンポーネントが見つかった場合、その部分のレンダリングはサーバー上では停止され、クライアントコンポーネントへの参照が特別なRSCペイロードに含まれます。サーバーコンポーネント自体は、そのクライアントコンポーネントに渡すpropsを含めてレンダリングを続けます。
- サーバー上でレンダリングされたサーバーコンポーネントの構造と、クライアントコンポーネントへの参照、および初期データなどが含まれたRSCペイロードが生成されます。このペイロードはHTMLではありません。
- 同時に、Next.jsのApp Routerは、従来のSSRと同様に、ルートの初期HTMLシェルを生成します。これは高速なファーストペイントのためです。このHTMLには、サーバーコンポーネントの一部(
Suspense
のフォールバックなど)や、レンダリングツリーの「シェル」が含まれることがあります。 - ブラウザは初期HTMLを受信し、すぐに表示します。
- ブラウザは、RSCペイロードを受信します。
- クライアントサイドのReactランタイムがRSCペイロードを解釈し、UIツリーを構築します。ここで、クライアントコンポーネントの参照が見つかった場合、対応するJavaScriptコードがダウンロードされ、クライアント側でコンポーネントがレンダリング・ハイドレーションされます。
- UIが完全に表示され、インタラクティブになります。
このプロセスは、ストリーミングを利用することでさらに効率化されます。サーバーはRSCペイロードを一度に全て送信するのではなく、レンダリングが完了した部分から順次クライアントにストリーム送信できます。クライアントはそれを受け取り次第、UIの該当部分をレンダリングします。これにより、データ取得に時間のかかる部分があっても、ページの他の部分はすぐに表示され始め、ユーザーは待たされている感覚を軽減できます。
3. RSCがもたらすReact開発の変化
RSCは、従来のReact開発パラダイムにいくつかの重要な変化をもたらします。
3.1. パフォーマンスの向上
3.1.1. JavaScriptバンドルサイズの削減
これはRSCの最も大きな利点の一つです。サーバーコンポーネントのコードはクライアントに送信されません。これにより、以下のようなコードをクライアントバンドルから完全に排除できます。
- データ取得ライブラリ(例:
node-fetch
, データベースクライアント) - マークダウンパーサーや日付フォーマットライブラリなど、サーバーでのみ必要なユーティリティ
- 認証情報やAPIキーなど、秘密情報を含むロジック
- 静的なUIコンポーネントのコード自体
クライアントがダウンロードしてパース・実行する必要があるJavaScript量が減るため、ページのロード時間が短縮され、特に低速なネットワーク環境やリソースに制約のあるデバイスでのパフォーマンスが大幅に向上します。
3.1.2. 高速な初期表示とインタラクティブ化
SSRと組み合わせることで、RSCは高速な初期表示を実現します。さらに、クライアントコンポーネントのJavaScriptのみがダウンロードされればよいため、ハイドレーションに必要なJS量が減り、TTIも改善されます。
3.1.3. データ取得の効率化
サーバーコンポーネント内で直接 await
を使用してデータ取得できることは、開発者にとって非常にシンプルです。クライアントサイドで useEffect
とローディングステートを組み合わせてデータ取得を行う必要がなくなります。
“`javascript
// 従来のClient Componentでのデータ取得
‘use client’;
import { useState, useEffect } from ‘react’;
function Post({ postId }) {
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
const res = await fetch(/api/posts/${postId}
);
const data = await res.json();
setPost(data);
setLoading(false);
}
fetchData();
}, [postId]);
if (loading) return
Loading…
;
if (!post) return
Post not found
;
return (
{post.title}
{post.content}
);
}
“`
``javascript
${process.env.API_URL}/posts/${postId}`);
// RSCでのデータ取得 (Next.js App Router)
async function Post({ postId }) {
// サーバー上で直接データをフェッチ
const res = await fetch(
const post = await res.json();
if (!post) return
Post not found
;
return (
{post.title}
{post.content}
);
}
“`
RSCでは、コンポーネントのレンダリングとデータ取得がサーバー側で密接に連携します。Next.jsのApp Routerでは、fetch
APIが自動的に拡張され、リクエストのメモ化やキャッシュ、再検証(revalidation)といった機能が提供されます。これにより、データ取得のウォーターフォールを防ぎつつ、効率的にデータを扱えます。
また、Suspense
と組み合わせることで、データ取得中のローディングステートをより宣言的に扱うことができます。データ取得中のコンポーネントはサスペンドし、その親の Suspense
バウンダリで指定されたフォールバックが表示されます。
3.2. 開発体験の向上(特定タスクにおいて)
データ取得をサーバーコンポーネントで行えるようになったことで、クライアントコンポーネントはUIのインタラクティブ性やState管理に集中できます。これは、関心の分離を促進し、コンポーネントの役割を明確にします。
特に、静的なコンテンツやデータ表示が中心のコンポーネントでは、useState
や useEffect
といったフックを使わずに済むため、コードがシンプルになります。
3.3. セキュリティの向上
APIキー、データベース認証情報、外部サービスのシークレットキーなどの機密情報は、サーバーコンポーネント内であれば安全に扱えます。これらの情報は決してクライアントに送信されないため、情報漏洩のリスクが低減します。
javascript
// サーバーコンポーネント内での安全なAPIキー使用
async function DataFetchingComponent() {
// API_KEYはサーバーの環境変数から取得
const apiKey = process.env.EXTERNAL_API_KEY;
const res = await fetch(`https://external.api.com/data?key=${apiKey}`);
const data = await res.json();
// ... render data
}
従来のCSRやSSRでは、クライアントから直接APIを呼び出す場合、APIキーを環境変数に入れてもビルド時にクライアントバンドルに含まれてしまうリスクがありましたが、RSCではこのような心配がなくなります。
3.4. ストリーミングによるユーザー体験の向上
RSCのレンダリング結果とペイロードはストリーミングが可能です。これは、データ取得に時間のかかる部分がある場合でも、既にサーバーでレンダリングが完了した他の部分から順次クライアントに送信され、表示を開始できることを意味します。
例えば、ページのヘッダーやサイドバーのような高速に準備できる部分はすぐに表示し、時間がかかるメインコンテンツの読み込み中はローディングスピナーを表示するといったことが、Suspense
を使うことで容易に実現できます。これにより、ユーザーはページのロードが進行していることを視覚的に確認でき、待ち時間を短く感じることができます。
3.5. SEOとOGPの改善
RSCはSSRと組み合わせて使用されることが一般的です(例: Next.js App Router)。これにより、サーバーでレンダリングされた初期HTMLがブラウザに送信されるため、検索エンジンのクローラーがコンテンツを適切にインデックスできるようになります。OGP(Open Graph Protocol)タグなどもサーバー側で動的に生成しやすくなります。
3.6. サーバーアクション (Server Actions) との連携
RSCの導入と並行して、Reactとフレームワークはサーバーアクションという概念も推進しています。サーバーアクションは、クライアントから直接サーバーサイドのコード(非同期関数)を呼び出すための仕組みです。これは、フォーム送信やボタンクリックなどのユーザーインタラクションによって、データベースの更新やファイル操作といったサーバーサイドの処理を実行したい場合に非常に便利です。
従来のSPA開発では、クライアントからサーバーのAPIエンドポイントに対してHTTPリクエスト(POST, PUTなど)を送信する必要がありましたが、サーバーアクションを使えば、クライアントコンポーネントからサーバーコンポーネント内に定義された非同期関数を直接呼び出すかのような記述が可能になります。
“`javascript
// Server Component (または Server Actionを含むファイル)
‘use server’; // このディレクティブが必要
import { savePost } from ‘@/lib/posts’;
export async function createPost(formData) {
const title = formData.get(‘title’);
const content = formData.get(‘content’);
// サーバー側で安全に処理
await savePost({ title, content });
}
“`
“`javascript
// Client Component
‘use client’;
import { createPost } from ‘@/app/actions’; // サーバーアクションをインポート
function NewPostForm() {
return (
// フォームアクションとしてサーバーアクションを直接指定
);
}
“`
サーバーアクションはRSCと密接に関連しており、クライアントコンポーネントからサーバーサイドの変更操作を簡単に行えるようにすることで、RSCモデルでの開発体験を向上させます。データ取得はサーバーコンポーネントで、データの変更はサーバーアクションで、という役割分担が明確になります。
4. RSC導入における課題と考慮事項
RSCは多くの利点をもたらしますが、導入にあたってはいくつかの課題や考慮すべき点があります。
4.1. パラダイムシフトと学習コスト
最も大きな変化は、開発者が「コンポーネントがどこで実行されるか」を常に意識する必要がある点です。従来のReact開発者は、ほとんどのコードがクライアントで実行されることを前提としていました。RSCでは、サーバーコンポーネントとクライアントコンポーネントの明確な区別、それぞれの制約、そして相互のやり取りのルールを理解する必要があります。
特に、StateやEffect、ブラウザAPIに依存する既存のライブラリは、クライアントコンポーネント内で使用する必要があります。また、クライアントコンポーネント内で定義されたStateは、そのコンポーネントのサブツリーでのみ有効になります。サーバーコンポーネントから直接クライアントコンポーネントのStateを操作することはできません。グローバルなState管理が必要な場合は、クライアントコンポーネントのルートに近い部分でState管理ライブラリをセットアップし、それを他のクライアントコンポーネントに渡す(Contextなどを使用)というパターンが考えられます。
この思考の切り替えには、ある程度の学習コストがかかります。
4.2. 既存ライブラリとの互換性
多くの既存のReactライブラリ(例: UIライブラリ、State管理ライブラリ、アニメーションライブラリ)は、クライアントサイドでの実行、StateやEffectの使用、ブラウザAPIへのアクセスを前提として開発されています。これらのライブラリは、RSC環境では 'use client'
を持つクライアントコンポーネント内で使用する必要があります。
ライブラリ自体がRSCフレンドリーに対応しているかどうかも重要です。例えば、一部のUIライブラリは、コンポーネントがサーバーコンポーネントとしてレンダリングされることを想定していません。ヘッドレスUIライブラリなど、レンダリングにStateやEffectを多用しないライブラリは、RSC環境でも比較的簡単に利用できる傾向があります。ライブラリの作者がRSCを考慮して設計・アップデートしていくことが、エコシステム全体のRSC対応を進める上で重要です。
4.3. デバッグの複雑性
コードがサーバーとクライアントの両方で実行されるため、問題の発生箇所を特定するのが従来の開発よりも複雑になる可能性があります。サーバーサイドのログとクライアントサイドのコンソールログの両方を確認し、RSCペイロードのやり取りも理解する必要が出てくるかもしれません。フレームワークやツールのデバッグ機能の進化が期待されます。
4.4. サーバー負荷の増加
データ取得やレンダリングの一部をサーバーに移行するため、サーバーリソースの使用量が増加する可能性があります。特にトラフィックが多いアプリケーションや、データ取得・レンダリングに時間のかかるコンポーネントが多い場合、サーバーのスケールが必要になるかもしれません。これは従来のSSRアプリケーションでも同様の課題ですが、RSCではより多くのレンダリングワークをサーバーが担当する可能性があります。
4.5. キャッシュ戦略の再検討
RSCの優れた点の1つは、fetch
APIの拡張による強力なキャッシュ機能です。しかし、これは同時に、データのキャッシュがどのように行われ、いつ無効化されるのか(再検証 – revalidation)を正確に理解する必要があることを意味します。意図しない古いデータが表示されたり、逆に頻繁にデータが取得されすぎたりしないよう、キャッシュ戦略を適切に設計・管理する必要があります。
4.6. SSRとの役割分担の理解
RSCはSSRを置き換えるものではなく、補完するものです。Next.js App RouterのようなRSCをサポートするフレームワークは、多くの場合、初期の高速表示のためにSSR(サーバーでのHTML生成)を行い、その後の動的なコンテンツやインタラクティブ性のためにRSCペイロードのストリーミングとクライアントでのレンダリング・ハイドレーションを行います。この二つのレンダリング手法の役割分担と連携を理解することが重要です。RSCペイロードはHTMLそのものではないため、RSC単体では初期の完全なHTMLを生成できません。
5. RSCの具体的な導入方法(Next.js App Routerを例に)
現時点で、RSCを最も成熟した形でサポートしているフレームワークは、Next.jsのApp Routerです。App Routerは、デフォルトでサーバーコンポーネントをベースとした設計になっており、RSCのコンセプトを最大限に活かせるようになっています。
ここでは、Next.js App RouterにおけるRSCの基本的な使い方を解説します。
5.1. ファイル規約
App Routerでは、ファイル名の規約によってサーバーコンポーネントかクライアントコンポーネントかが区別されます。
.js
,.jsx
,.ts
,.tsx
(ルート、レイアウト、ページなど): デフォルトでサーバーコンポーネントです。これらのファイルはサーバー上で実行されます。'use client';
ディレクティブを持つファイル: 明示的にクライアントコンポーネントとして指定されます。ファイルの先頭に'use client';
と記述します。
これにより、開発者は特別なファイル拡張子を使う必要なく、ファイルの内容によってコンポーネントのタイプを区別できます。ただし、コードを整理するために、components
ディレクトリ内に *.client.tsx
や *.server.tsx
といった命名規約を採用することも一般的です(これはNext.jsの規約ではなく、コミュニティの慣習です)。
5.2. サーバーコンポーネントでのデータ取得
サーバーコンポーネントは async
として定義し、その中で await
を使って非同期処理を待つことができます。
“`javascript
// app/page.tsx (Server Component)
async function getData() {
// このfetchはサーバー上で実行される
const res = await fetch(‘https://api.example.com/data’);
// エラーハンドリングは適切に行う
if (!res.ok) {
throw new Error(‘Failed to fetch data’);
}
return res.json();
}
export default async function Page() {
const data = await getData(); // サーバー上でデータを取得
return (
Welcome
Data from server: {data.message}
{/ Client Component をレンダリング /}
);
}
“`
ここでは、getData
関数内で fetch
が呼び出されていますが、この fetch
はブラウザではなくサーバー上で実行されます。環境変数(process.env.API_URL
など)を使ってサーバーサイドのエンドポイントを安全に指定することも可能です。
5.3. クライアントコンポーネントの作成と使用
インタラクティブな要素やStateが必要な場合は、クライアントコンポーネントを作成します。
“`javascript
// components/interactive-counter.tsx
‘use client’; // これがクライアントコンポーネントであることを示す
import { useState } from ‘react’;
export default function InteractiveCounter() {
const [count, setCount] = useState(0);
return (
Count: {count}
);
}
“`
この InteractiveCounter
コンポーネントは、先ほどの app/page.tsx
のサーバーコンポーネントからインポートされ、レンダリングされています。サーバーコンポーネントはクライアントコンポーネントをインポート・レンダリングできます。
5.4. クライアントコンポーネント内でのデータ取得
クライアントコンポーネントでもデータ取得は可能ですが、それはあくまでクライアントサイドでのフェッチになります。例えば、ユーザーのアクションに応じて追加のデータを取得する場合などに使用します。ただし、初期ロード時に必要なデータ取得は、可能な限り親のサーバーコンポーネントで行うのがRSCの思想です。
“`javascript
// components/client-data-fetch.tsx
‘use client’;
import { useEffect, useState } from ‘react’;
export default function ClientDataFetch({ userId }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
async function fetchUserData() {
const res = await fetch(/api/users/${userId}
); // クライアント側でAPIを叩く
const data = await res.json();
setUserData(data);
}
fetchUserData();
}, [userId]);
if (!userData) return
Loading user data…
;
return (
User: {userData.name}
{/ … /}
);
}
“`
このように、同じ fetch
であっても、サーバーコンポーネント内で使用するかクライアントコンポーネント内(useEffect
など)で使用するかで、実行される場所が変わります。
5.5. サーバーコンポーネントからクライアントコンポーネントへのデータの受け渡し
サーバーコンポーネントで取得したデータは、propsとして子であるクライアントコンポーネントに渡すことができます。
“`javascript
// app/post/[id]/page.tsx (Server Component)
import InteractiveCommentSection from ‘@/components/interactive-comment-section’;
async function getPost(postId) {
const res = await fetch(${process.env.API_URL}/posts/${postId}
);
return res.json();
}
async function getComments(postId) {
const res = await fetch(${process.env.API_URL}/posts/${postId}/comments
);
return res.json();
}
export default async function PostPage({ params }) {
const postId = params.id;
// サーバーで投稿とコメントを並列取得
const [post, comments] = await Promise.all([
getPost(postId),
getComments(postId)
]);
if (!post) return
Post not found
;
return (
{post.title}
{post.content}
<h2>Comments</h2>
{/* サーバーで取得したデータをクライアントコンポーネントに渡す */}
<InteractiveCommentSection initialComments={comments} postId={postId} />
</article>
);
}
“`
“`javascript
// components/interactive-comment-section.tsx
‘use client’;
import { useState } from ‘react’;
// import { addCommentServerAction } from ‘@/app/actions’; // Server Actionを使う場合
export default function InteractiveCommentSection({ initialComments, postId }) {
const [comments, setComments] = useState(initialComments);
const [newComment, setNewComment] = useState(”);
// コメント追加処理(例:Server Actionを使う場合)
const handleAddComment = async () => {
// await addCommentServerAction(postId, newComment);
// コメントリストを更新
// …コメントリストの再フェッチやStateの更新…
};
return (
-
{comments.map(comment => (
- {comment.text}
))}
{/ コメント入力フォーム /}
setNewComment(e.target.value)}
placeholder=”Add a comment”
/>
);
}
“`
この例では、PostPage
(サーバーコンポーネント) が投稿データとコメントリストの両方をサーバー側で取得し、コメントリストを initialComments
というprops名で InteractiveCommentSection
(クライアントコンポーネント) に渡しています。InteractiveCommentSection
はその初期データを受け取り、ユーザーのインタラクションに応じてStateを管理したり、新しいコメントを追加する処理(ここでは省略されていますが、Server Actionなどが候補)を実行したりします。
重要なのは、クライアントコンポーネントに渡すpropsは、サーバーコンポーネントからクライアントにシリアライズされて送られるため、シリアライズ可能なデータである必要があるという点です。関数、クラスインスタンス、Symbolなどは直接propsとして渡せません(ただし、Next.jsのServer Actionsは関数の形で渡せるように特別な処理が行われています)。
5.6. クライアントコンポーネントからサーバーコンポーネントを「使う」方法
前述の通り、クライアントコンポーネントはサーバーコンポーネントを直接インポート・レンダリングできません。しかし、サーバーコンポーネントをpropsとして子要素として受け取ることは可能です。これは、サーバーコンポーネントが「より高い階層」にあり、クライアントコンポーネントが「より低い階層」にある場合に、サーバーコンポーネントのレンダリング結果をクライアントコンポーネントの内部に「埋め込む」ようなイメージです。
“`javascript
// components/layout.tsx (Client Component)
‘use client’;
import { useState } from ‘react’;
export default function Layout({ children }) { // children は Server Component か Client Component かは問わない
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
{sidebarOpen &&
}
{children} {/ ここにServer Componentがレンダリングされる /}
);
}
“`
“`javascript
// app/page.tsx (Server Component)
// このファイルには ‘use client’ は不要なので Server Component
import Layout from ‘@/components/layout’;
import PostList from ‘@/components/post-list’; // PostListもServer Componentと仮定
export default function Page() {
return (
// LayoutはClient Componentだが、そのchildrenとしてServer Componentである
Latest Posts
);
}
“`
このパターンは非常に強力で、クライアントサイドのインタラクティブなレイアウトコンポーネント(例: サイドバーの開閉、テーマ切り替え)の中に、サーバーサイドでレンダリングされたコンテンツ(例: データ一覧、ブログ記事)を埋め込むことができます。データ取得や重い処理はサーバーコンポーネントで行い、純粋なインタラクションだけをクライアントコンポーネントで担当するという、RSCの哲学に沿った構造になります。
6. RSCと従来のレンダリング手法の比較
RSCが登場したことで、従来のCSR、SSR、SSGといった手法との関係性がどうなるのかが気になる点です。RSCはこれらを置き換えるものではなく、補完し、より柔軟な選択肢を提供するものです。
特徴 | CSR (Create React Appなど) | SSR (Next.js Pages Routerなど) | SSG (Gatsby, Next.js Pages Router export) | RSC (Next.js App Router) |
---|---|---|---|---|
実行場所 | クライアント | サーバー (初期), クライアント | ビルド時 | サーバー (大部分), クライアント (インタラクティブ部分) |
初期HTML | minimal (JSに依存) | full (データ含む) | full (データ含む) | shell + partial streams |
JSバンドルサイズ | 大 | 大 | 中〜大 | 小 (サーバー部分削減) |
データ取得 | クライアント (useEffect ) |
サーバー (getServerSideProps ) |
ビルド時 (getStaticProps ) |
サーバー (async component , fetch ) |
State/Effect | ⭕️ 全てのコンポーネント | ⭕️ 全てのコンポーネント (ハイドレーション後) | ⭕️ 全てのコンポーネント (ハイドレーション後) | ⭕️ クライアントコンポーネントのみ |
インタラクティブ性 | ハイドレーション後すぐに | ハイドレーション後 | ハイドレーション後 | クライアントコンポーネントのみ |
SEOフレンドリー | △ (クローラーによる) | ⭕️ | ⭕️ | ⭕️ (SSRと組み合わせ) |
ビルド時間 | 短 | 短〜中 (リクエスト時レンダリング) | 長 (全ページ生成) | 中〜長 |
デプロイ | 静的ホスティング | Node.jsサーバーなど | 静的ホスティング | Node.jsサーバーなど (Vercel推奨) |
RSCはSSRを置き換えるものではないという点が特に重要です。Next.js App RouterのようなRSC対応フレームワークは、多くの場合、初期レンダリングをSSRで行い、完全なHTMLシェルを生成します。その後、RSCのストリーミングペイロードがクライアントに送られ、動的な部分やインタラクティブな部分がクライアント側で構築・ハイドレーションされます。RSCは、コンポーネントごとの最適なレンダリング場所を選択できるという点において、SSRよりも粒度の細かい制御を提供します。SSRはページ単位で「全てのコンポーネントをサーバーでHTMLにレンダリングし、クライアントでハイドレーションする」というモデルですが、RSCは「サーバーでレンダリングできる部分はサーバーで、クライアントでしかできない部分はクライアントで」という、コンポーネントレベルでの分割を可能にします。
これにより、サーバーでレンダリングされた部分のJSコードをクライアントに送る必要がなくなり、結果としてJSバンドルサイズを削減できます。
静的なサイトには依然としてSSGが最適な選択肢となる場合があります。非常に動的なページや、ユーザーごとにコンテンツが大きく異なるページにはSSRやRSCが適しています。RSCは、SSRの利点(高速な初期表示、SEO)を維持しつつ、CSRやSSRの欠点であったJSバンドルサイズやデータ取得の非効率性を改善する、新しいアプローチと言えます。
7. RSCの活用事例
どのような場面でサーバーコンポーネントを使い、どのような場面でクライアントコンポーネントを使うべきか、具体的な例を挙げます。
サーバーコンポーネントを使用するのに適した場面:
- データ表示: ブログ記事、商品リスト、ユーザープロフィールなど、サーバーから取得したデータを表示するだけの部分。
- データベースやAPIへのアクセス: サーバー側でしかアクセスできないリソースからデータを取得する部分。
- 秘密情報の利用: APIキーなど、クライアントに公開できない情報を使う部分。
- 重い処理: マークダウンのパース、複雑な計算、ファイル操作など、サーバーで実行した方が効率的な処理。
- 静的なレイアウト: ヘッダー、フッター、サイドバーなど、ユーザーインタラクションが少なく、全ページで共通するような構造部分(ただし、これらの要素内にインタラクティブな要素が含まれる場合は、クライアントコンポーネントを組み合わせる必要があります)。
- 依存関係が大きいコンポーネント: サーバーでのみ必要な大きなライブラリ(例: 一部の画像処理ライブラリ、PDF生成ライブラリ)に依存するコンポーネント。
クライアントコンポーネントを使用するのに適した場面:
- ユーザーインタラクション: ボタンクリック、フォーム入力、スライダー操作など、ユーザーの操作に応じたUIの変更が必要な部分。
- State管理: フォームの状態、モーダルの表示/非表示、カートの中身など、コンポーネント内で状態を保持・更新する必要がある部分。
- ブラウザAPIの使用:
window
,document
,localStorage
, Geolocation APIなど、ブラウザ固有の機能にアクセスする必要がある部分。 - ライフサイクルイベント: マウント時/アンマウント時に処理を実行する (
useEffect
) 部分(例: アニメーションの開始/停止、外部ライブラリの初期化)。 - クライアントサイド専用ライブラリ: グラフ描画ライブラリ、ドラッグ&ドロップライブラリなど、クライアントサイドJSに強く依存するライブラリを使用する部分。
- Context APIなどを使ったグローバルなState管理: アプリケーション全体で共有するStateを管理し、複数のコンポーネントで更新・参照する必要がある部分(コンポーネントツリーの高い位置にある
'use client'
ファイルでProviderを定義することが多い)。
判断のヒント:
- そのコンポーネントはインタラクティブですか? StateやEffectが必要ですか? -> クライアントコンポーネントの可能性が高い。
- そのコンポーネントはデータを取得しますか? そのデータ取得はサーバーサイドのリソースに依存しますか? -> サーバーコンポーネントの可能性が高い。
- そのコンポーネントのコードや依存関係はクライアントに送られる必要がありませんか? -> サーバーコンポーネントの可能性が高い。
- StateやEffect、ブラウザAPIを使わずにレンダリングできますか? サーバーとクライアントの両方で共通して使われますか? -> 共有コンポーネント(ただし、State/Effectを使わないという制約付き)の可能性が高い。
原則として、可能な限りサーバーコンポーネントを使用するのがRSCの推奨されるアプローチです。クライアントコンポーネントは、本当にインタラクティブ性が必要な部分に限定することで、パフォーマンス上の利点を最大限に活かせます。
8. React開発の未来とRSC
React Server Componentsは、Reactの歴史における重要な進化です。これは単なる新しい機能追加ではなく、コンポーネントのレンダリング場所とデータフローに関するReactの根本的な考え方を拡張するものです。
RSCの導入により、これまでSSRやSSGといったフレームワークレベルの機能として扱われていたものが、Reactのコアライブラリの機能として提供されるようになります(RSC自体はReactライブラリの一部として実装されています)。これにより、Reactのエコシステム全体がRSCに対応し、より最適化されたライブラリやツールが生まれることが期待されます。
例えば、データ取得ライブラリはRSCの async
コンポーネントや拡張 fetch
に対応する必要が出てくるでしょう。State管理ライブラリは、クライアントコンポーネントの境界での利用を前提とした設計や、サーバーコンポーネントから初期データをどのように受け取るかといったパターンを提供する必要があるかもしれません。UIライブラリは、サーバーでレンダリングされる部分とクライアントでハイドレーションされる部分を考慮したコンポーネントを提供することが望ましくなります。
RSCはまだ比較的新しい技術であり、特にエコシステムの対応は進化の途上にあります。しかし、Next.js App Routerを皮切りに、他のフレームワーク(例: Remixの将来的な対応)やライブラリもRSCをサポートしていくことで、React開発の標準的なプラクティスになっていく可能性が高いです。
これにより、開発者はパフォーマンスと開発体験を両立させながら、より高機能で効率的なウェブアプリケーションを構築できるようになるでしょう。React開発者は、この新しいパラダイムに適応し、コンポーネントの適切な配置と役割分担を意識した設計を行うスキルを習得していくことが求められます。
9. まとめ
本記事では、React Server Components (RSC) が登場した背景、その仕組み、そしてReact開発に与える影響について詳細に解説しました。
- RSCは、コンポーネントをサーバー上でレンダリングし、その結果を特別なペイロードとしてクライアントに送信する新しいレンダリングモデルです。
- コンポーネントを「サーバーコンポーネント」「クライアントコンポーネント」「共有コンポーネント」に分類し、それぞれ実行場所や機能に違いがあります。
- RSCの主な利点は、JavaScriptバンドルサイズの劇的な削減、高速な初期表示とインタラクティブ化、サーバーサイドでの効率的かつ安全なデータ取得、ストリーミングによるユーザー体験の向上などです。
- 導入には、サーバーとクライアントの実行環境の違いを理解するパラダイムシフトや、既存ライブラリとの互換性、デバッグの複雑性といった課題も伴います。
- Next.js App Routerは、RSCを標準的にサポートしており、ファイル規約や
async
コンポーネント、'use client'
ディレクティブなどを使ってRSCを導入できます。 - クライアントコンポーネントはサーバーコンポーネントを直接インポートできませんが、propsとして子要素として受け取ることで連携します。
- RSCはSSRを置き換えるのではなく、補完する関係にあります。
- RSCはReact開発の未来を形作る重要な技術であり、エコシステム全体がRSCに対応していくことで、より効率的で高性能なウェブアプリケーション開発が可能になるでしょう。
RSCは、現代の複雑なウェブアプリケーションのパフォーマンスと開発効率の要求に応えるためのReactの回答です。最初は慣れが必要かもしれませんが、その強力な機能と利点を理解し、適切に活用することで、Reactアプリケーション開発は新たな段階へと進むでしょう。
今後のReact開発に携わる上で、RSCの理解は避けて通れない重要なステップとなります。本記事が、React開発者の皆様がRSCの世界へ踏み出すための一助となれば幸いです。