はい、承知いたしました。React Server Components (RSC) に関する約5000語の詳細な記事を作成します。
【RSC】React Server Componentsとは?導入方法と注意点
1. はじめに:React開発の新たな地平へ
Reactは、モダンなWebアプリケーション開発においてデファクトスタンダードとも言えるほど普及したJavaScriptライブラリです。コンポーネントベースの設計思想と宣言的なUI構築により、複雑なユーザーインターフェースの開発を効率化してきました。しかし、Reactを用いたシングルページアプリケーション(SPA)開発には、長年付きまとってきた課題が存在します。
代表的な課題として、以下のようなものが挙げられます。
- 初期表示速度の遅延: アプリケーションのJavaScriptバンドルサイズが大きくなると、ブラウザがスクリプトをダウンロード、パース、実行し、最初のレンダリングを行うまでの時間が長くなります(Time To Interactive – TTI)。これはユーザー体験を損なう大きな要因となります。
- 大量のJavaScriptコード: アプリケーションの機能が増えるにつれて、クライアントサイドで実行されるJavaScriptコードが増大します。これにより、バンドルサイズが肥大化し、ダウンロードと実行のコストが増加します。
- データ取得の非効率性: 多くのSPAでは、クライアントサイドでコンポーネントがマウントされてからAPIコールを行います。ネストされたコンポーネントがそれぞれデータを取得する場合、いわゆる「N+1問題」や「データ取得のウォーターフォール」が発生し、レンダリング完了までに時間がかかります。
- サーバーサイドレンダリング (SSR) や静的サイト生成 (SSG) の限界: 初期表示速度やSEOの課題を解決するためにSSRやSSGが導入されてきました。SSRはサーバーでHTMLを生成し、クライアントでハイドレーション(イベントハンドラーのアタッチなど)を行いますが、結局インタラクティブになるためにはクライアント側でJavaScriptを実行する必要があり、ハイドレーションのコストも無視できません。SSGはビルド時に静的なページを生成しますが、動的なコンテンツやユーザー固有のデータ表示には向いていません。
- APIキーなどの機密情報の取り扱い: クライアントサイドでデータ取得を行う場合、APIキーなどの機密情報をクライアントコードに含めることはセキュリティリスクとなります。
これらの課題に対し、Reactチームは全く新しいアプローチを提案しました。それが「React Server Components (RSC)」です。RSCは、コンポーネントの一部または全体をサーバー上でレンダリングし、その結果をクライアントに送信することで、前述の課題を根本的に解決しようと試みる革新的な技術です。
本記事では、React Server Componentsがどのようなものか、なぜ必要とされているのか、その仕組み、具体的な導入方法(特にNext.js App Routerを例に)、そして開発における注意点や制約について、詳細かつ網羅的に解説します。この記事を読むことで、RSCの概念を理解し、ご自身のプロジェクトへの導入を検討するための知識を得られるでしょう。
2. React Server Components (RSC) とは
2.1. RSCの定義と目的
React Server Componentsは、その名の通りサーバー上でレンダリングされるReactコンポーネントです。従来のReactコンポーネントがブラウザ(クライアントサイド)で実行されるのに対し、RSCはサーバーサイドで実行されます。
RSCの主な目的は以下の通りです。
- JavaScriptバンドルサイズの削減: サーバーでレンダリングされるため、RSC自体やRSCがインポートするモジュールはクライアントのJavaScriptバンドルに含まれません。これにより、ダウンロード、パース、実行のコストを大幅に削減できます。
- 初期表示速度の向上: サーバーでデータを取得し、レンダリング結果を直接クライアントに送信できるため、クライアント側でのデータ取得や追加のレンダリングを待つ必要がありません。ストリーミングによる高速な応答も可能です。
- データ取得の効率化: サーバー上でデータ取得を完結させることができます。クライアントコンポーネントがマウントされるのを待たずに、サーバーコンポーネント内で直接データベースや外部APIにアクセスし、データを取得できます。これにより、データ取得のウォーターフォールを防ぎ、パフォーマンスを向上させます。
- 機密情報の安全な取り扱い: APIキーやデータベース接続情報などの機密情報をサーバーコンポーネント内に閉じることで、クライアントサイドに漏洩するリスクを排除できます。
- サーバーリソースの活用: サーバーの計算リソースを活用して、コストの高い処理(マークダウンのパース、画像の変換など)をサーバーで行い、クライアントの負担を軽減できます。
2.2. 従来のReactコンポーネント(クライアントコンポーネント)との違い
RSCを理解する上で最も重要なのは、従来のReactコンポーネント、すなわち「クライアントコンポーネント」との違いを明確にすることです。
特徴 | React Server Components (RSC) | React Client Components (RCC) |
---|---|---|
実行環境 | サーバー | ブラウザ(クライアント) |
ファイル拡張子 | .server.js (またはフレームワークの規約) |
.client.js (またはフレームワークの規約) |
デフォルト | Next.js App Routerなどではデフォルト | 従来のReact開発ではデフォルト |
インタラクティビティ | なし(イベントハンドラーを持てない) | あり(イベントハンドラー、Hooks使用可) |
状態管理 | 不可 (useState , useReducer など使用不可) |
可 (useState , useReducer など使用可) |
副作用 | 不可 (useEffect , useLayoutEffect など使用不可) |
可 (useEffect , useLayoutEffect など使用可) |
ブラウザAPI | 使用不可 (window , document など) |
使用可 (window , document など) |
データ取得 | サーバーサイドで直接データソースにアクセス | クライアントサイドでAPIをコール |
バンドルサイズ | ゼロ (クライアントのJSバンドルに含まれない) | クライアントのJSバンドルに含まれる |
Props | シリアライズ可能な値、または他のコンポーネント | シリアライズ可能な値、関数、オブジェクトなど |
Context API | 使用に制約あり(特別な仕組みが必要) | 標準的に使用可 |
この表から分かるように、RSCとクライアントコンポーネントは役割が全く異なります。
- RSC: データ取得、機密性の高い処理、サーバーでの静的な(または静的に近い)マークアップ生成に適しています。インタラクティブな要素は持ちません。
- クライアントコンポーネント: ユーザーとのインタラクション、状態管理、ブラウザAPIへのアクセス、副作用の実行など、クライアントサイドでの動的な振る舞いに必要です。従来のReactコンポーネントの役割を引き継ぎます。
実際のアプリケーションでは、これら2種類のコンポーネントを組み合わせて使用します。サーバーコンポーネントでページの基本的な構造とデータを取得し、インタラクティブな部分が必要な箇所だけクライアントコンポーネントを使用するというのが基本的なパターンです。
3. なぜRSCが必要なのか?従来のSPA、SSR、SSGとの比較
前述のように、RSCは従来のWeb開発手法が抱える課題を解決するために登場しました。ここでは、既存の手法と比較しながら、RSCの必要性をさらに掘り下げます。
3.1. 従来のSPAの課題(再確認)
現代の多くのSPAは、クライアントサイドで全てのレンダリングを行います(CSR – Client-Side Rendering)。
- ユーザーがURLにアクセス。
- サーバーは最低限のHTML(通常は
<div id="root"></div>
のようなプレースホルダーとJavaScriptへの参照)を返す。 - ブラウザはHTMLをパースし、JavaScriptファイルをダウンロードする。
- JavaScriptがパースされ、実行される。
- Reactが起動し、APIをコールしてデータを取得する。
- データが取得できたら、Reactが仮想DOMを構築し、DOMを更新してUIをレンダリングする。
- ハイドレーションが行われ、イベントハンドラーがアタッチされる。
- ようやくページがインタラクティブになる(TTI)。
このプロセスでは、JavaScriptのダウンロードと実行が完了するまで何も表示されないか、最低限のローディング表示しか行われません。また、アプリケーションが大規模になるほどJavaScriptバンドルが肥大化し、ダウンロード・実行時間がさらに増加します。さらに、APIコールのウォーターフォールもパフォーマンスのボトルネックとなります。
3.2. SSR (Server-Side Rendering) との違い
SSRは、初期表示速度とSEOを改善するために登場しました。
- ユーザーがURLにアクセス。
- サーバーはReactコンポーネントをサーバーサイドでレンダリングし、最初のHTMLを生成して返す。
- ブラウザはHTMLを表示するため、ユーザーはすぐにコンテンツを見ることができる(First Contentful Paint – FCP)。
- ブラウザはJavaScriptファイルをダウンロードし、パース、実行する。
- Reactがクライアントサイドで起動し、サーバーで生成されたHTMLと仮想DOMの状態を同期させる(ハイドレーション)。
- ハイドレーションが完了すると、ページがインタラクティブになる(TTI)。
SSRは初期表示速度を改善しますが、いくつかの課題があります。
- ハイドレーションのコスト: クライアントサイドで再びReactを起動し、サーバーで生成されたDOM構造とクライアントサイドの仮想DOMを同期させるハイドレーション処理が必要です。この処理はコストが高く、完了するまでページは完全にインタラクティブになりません。
- JavaScriptバンドルは必要: サーバーでHTMLを生成しても、インタラクティブな要素やクライアントサイドのロジックのために、依然として大量のJavaScriptをクライアントに送信する必要があります。
- サーバー負荷: リクエストごとにサーバーでレンダリングを行うため、サーバーの負荷が高くなる可能性があります。
- データ取得の課題: サーバーでデータ取得を行う必要がありますが、ハイドレーションに必要なデータはクライアントにも渡す必要があります。また、クライアントコンポーネントでの追加のデータ取得はウォーターフォールを引き起こす可能性があります。
3.3. SSG (Static Site Generation) との違い
SSGは、ビルド時に全てのページを静的なHTMLファイルとして生成します。
- ビルド時にデータ取得とレンダリングを行い、静的なHTMLファイルを生成。
- ユーザーがURLにアクセスすると、サーバーは生成済みのHTMLファイルを返す。
- ブラウザはHTMLを表示する。
- クライアントサイドのJavaScriptがロード・実行され、ハイドレーションが行われる(インタラクティブになる)。
SSGは最も高速な初期表示と低コストでの配信(CDN利用など)を実現できます。しかし、以下のような限界があります。
- 動的なコンテンツへの不向き: ビルド時以降に更新されるデータや、ユーザーごとに異なるコンテンツを表示するページには向いていません。動的な部分を後からクライアントサイドでフェッチする必要があります。
- 大規模サイトでのビルド時間: ページ数が多いサイトではビルドに時間がかかることがあります。
3.4. RSCが提供するメリット
RSCは、これらの手法の利点を組み合わせ、欠点を補うことを目指します。
- ゼロバンドルサイズ: RSC自体はクライアントのJSバンドルに含まれないため、純粋なサーバーコンポーネントだけで構成される部分は、クライアントに送られるJSを一切増やしません。
- サーバーでのデータ取得: データ取得はサーバー上で完結し、ネットワーク遅延を最小限に抑えつつ、ウォーターフォールを防ぎます。
- ストリーミングレンダリング: サーバーはレンダリング結果をストリームとしてクライアントに送信できます。これにより、一部のコンテンツがレンダリングでき次第すぐにブラウザが表示を開始でき、体感的なパフォーマンスが向上します。SSRのようにハイドレーションを待つ必要がありません。
- 選択的なハイドレーション: クライアントコンポーネントだけがハイドレーションの対象となります。ページ全体ではなく、インタラクティブな部分のみにコストがかかります。
- クライアントとサーバーのシームレスな連携: 同じReactのコンポーネントモデルを使用しながら、サーバーとクライアントの境界を意識的に設計できます。
要するに、RSCはサーバーで実行するべきこと(データ取得、計算、静的なマークアップ)とクライアントで実行するべきこと(インタラクション、状態管理)を、コンポーネント単位で明確に分離し、それぞれの強みを最大限に活かすための仕組みです。これにより、高速な初期表示、少ないJavaScriptバンドル、高い開発効率を両立させることが可能になります。
4. RSCの仕組み
RSCは、従来のWeb開発とは異なる新しいアーキテクチャを採用しています。その核となる仕組みを理解することは、RSCを効果的に使う上で不可欠です。
4.1. ファイル拡張子による区別(フレームワーク依存)
React本体はファイル拡張子による区別を推奨していましたが、現在主流のRSCをサポートするフレームワーク(特にNext.js App Router)では、より統合されたアプローチが取られています。
- デフォルト: Next.js App Routerの
app
ディレクトリ内では、全てのコンポーネントはデフォルトでサーバーコンポーネントとして扱われます。 use client
ディレクティブ: ファイルの先頭に'use client';
と記述することで、そのコンポーネントとそのインポート先全てをクライアントコンポーネントとして明示的に指定します。use server
ディレクティブ: 関数(特にフォームのアクションなど)の先頭に'use server';
と記述することで、その関数をサーバーアクションとして定義します。これはクライアントコンポーネントからサーバーサイドのコードを安全に呼び出すための仕組みです。
このファイルシステムベースのルーティングと規約により、開発者はコンポーネントがサーバーで実行されるかクライアントで実行されるかを明確に意識して開発を進めることになります。
4.2. コンポーネントツリーの構造
RSC環境では、アプリケーションのコンポーネントツリーはサーバーコンポーネントとクライアントコンポーネントが混在する形になります。
基本的なルールは以下の通りです。
- サーバーコンポーネントはクライアントコンポーネントをレンダリングできる: サーバーコンポーネントのJSX内でクライアントコンポーネントを子要素やプロパティとして記述できます。サーバーはクライアントコンポーネントの参照をペイロードに含めてクライアントに送ります。
- クライアントコンポーネントはサーバーコンポーネントをレンダリングできない: クライアントコンポーネントのJSX内で直接サーバーコンポーネントをレンダリングすることはできません。ただし、親のサーバーコンポーネントから子要素として渡されたサーバーコンポーネントは、クライアントコンポーネント内で表示できます。これは、クライアントコンポーネントが「ブラックボックス」として受け取ったサーバーコンポーネントのレンダリング結果(ペイロードの一部)を表示するだけであり、クライアント自身がサーバーコンポーネントを実行するわけではないためです。
この制限により、インタラクティブなクライアントコンポーネントがデータ取得やサーバーサイドの処理を行うサーバーコンポーネントに依存するという、自然な構造が促進されます。
4.3. RSCペイロード
サーバーコンポーネントがレンダリングされると、その結果はHTMLではなく、特別なRSCペイロードという形式でクライアントに送信されます。このペイロードは、Reactがクライアントサイドでコンポーネントツリーを再構築し、表示するために必要な全ての情報を含んでいます。
RSCペイロードは以下のような要素で構成されます(これは概念的な表現であり、実際の形式はバイナリに近いかもしれません)。
- レンダリングされたJSX要素の構造
- 各要素のプロパティ(シリアライズ可能な値)
- クライアントコンポーネントへの参照(識別子)
- クライアントコンポーネントに渡されるプロパティ(シリアライズ可能な値または子要素としての他のコンポーネント)
- データ(サーバーコンポーネントが取得したデータなど)
- サーバーアクションへの参照
このペイロードはストリーミングで送信されるため、サーバーはレンダリングが完了した部分から順次クライアントにデータを送ることができます。これにより、クライアントは全てのデータとレンダリング結果が揃うのを待つことなく、受信したペイロードから段階的にUIをレンダリングできます。
4.4. Reactクライアントランタイムの役割
クライアントサイドでは、特別なReactクライアントランタイムが動作します。このランタイムは以下の役割を担います。
- RSCペイロードの受信: サーバーからストリームとして送信されるRSCペイロードを受信します。
- ペイロードの解析: 受信したペイロードを解析し、コンポーネントツリーの構造やデータ、クライアントコンポーネントへの参照などを解釈します。
- コンポーネントツリーの構築: 受信した情報をもとに、メモリ上でコンポーネントツリーを構築します。サーバーコンポーネントの部分はペイロードの構造から直接構築し、クライアントコンポーネントの部分は参照情報から対応するクライアントコンポーネントのコードをロードして配置します。
- レンダリング: 構築したコンポーネントツリーに基づいてDOMをレンダリングします。
- クライアントコンポーネントのハイドレーション: クライアントコンポーネントの部分に対してハイドレーションを実行し、イベントハンドラーをアタッチするなどしてインタラクティブにします。
この仕組みにより、クライアントはサーバーコンポーネントのコード自体を持つ必要がなく、レンダリング結果とクライアントコンポーネントのコードだけをダウンロードすれば良いことになります。
4.5. データ取得のライフサイクル
RSCにおけるデータ取得は、従来のクライアントサイドでのデータ取得とは大きく異なります。
- リクエスト: ユーザーがページにアクセスまたはクライアントサイドナビゲーションが発生。
- サーバーでのレンダリング: サーバーはリクエストに対応するルートのサーバーコンポーネントを特定し、レンダリングを開始。
- データ取得: サーバーコンポーネント内で
async/await
を用いて非同期処理(例:データベースクエリ、APIコール)を実行。Reactはデータが取得できるまでレンダリングを待機(またはサスペンドし、フォールバックを表示してからデータ取得完了後に再開)。 - レンダリング続行: データ取得が完了すると、サーバーは取得したデータを使ってコンポーネントをレンダリング。
- ペイロード送信: レンダリング結果をRSCペイロードとしてクライアントにストリーム送信。
- クライアントでの表示: クライアントは受信したペイロードを解析し、UIをレンダリング。データは既にペイロードに含まれているため、クライアントサイドでの追加のデータ取得は不要(ただし、クライアントコンポーネント内で動的にデータをフェッチすることは可能)。
この「レンダー中に非同期処理を待つ」というモデルは、ReactのSuspenseと深く連携しています。サーバーコンポーネント内で await
する際、Reactはサスペンドし、親コンポーネントで定義されたSuspenseフォールバック(ローディング表示など)を表示できます。データ取得が完了すると、フォールバックが実際のコンテンツに置き換わります。この仕組みにより、データ取得とUIのレンダリングを緊密に統合し、よりスムーズなユーザー体験を提供できます。
5. RSCの導入方法(Next.js App Routerを例に)
RSCをプロダクションで使用する場合、現時点ではNext.jsのApp Routerが最も一般的で成熟したフレームワークです。ここでは、Next.js App Routerを使ったRSCの基本的な導入方法と開発パターンを解説します。
5.1. Next.js App Routerのセットアップ
まず、最新版のNext.jsを使用してプロジェクトを作成します。プロジェクト作成時にApp Routerを使用するかどうかを選択するプロンプトが表示されますので、Yes
を選択します。
bash
npx create-next-app@latest my-rsc-app
プロジェクトが作成されたら、app
ディレクトリが作成されていることを確認してください。この app
ディレクトリがApp Routerのルートディレクトリとなり、この中に配置されるコンポーネントは、特別な指定がない限りデフォルトでサーバーコンポーネントとして扱われます。
5.2. デフォルトはサーバーコンポーネント
app
ディレクトリ内に新しくファイルを作成し、コンポーネントを定義してみましょう。例えば、app/page.tsx
(または.js
)というファイルを作成します。これはデフォルトでそのルートのトップレベルのページコンポーネントになります。
“`jsx
// app/page.tsx
import Link from ‘next/link’;
export default function HomePage() {
// このコンポーネントはデフォルトでサーバーコンポーネントです
// useState, useEffect, ブラウザAPIなどは使えません
const currentTime = new Date().toLocaleTimeString();
console.log(HomePage rendered on server at ${currentTime}
); // サーバーのログに出力される
return (
Welcome to My RSC App
This page is rendered on the server.
Current server time: {currentTime}
About Page
);
}
“`
このコンポーネントはサーバー上で実行され、生成されたHTML(厳密にはRSCペイロード)がクライアントに送られます。開発サーバーを起動し、ブラウザで確認してみてください。ソースコードを表示しても、このコンポーネントのJavaScriptコードは含まれていないことがわかります(Next.jsが必要とする最小限のクライアントランタイムはロードされます)。
5.3. use client
ディレクティブ:クライアントコンポーネントの使用
ユーザーとのインタラクションや状態管理が必要な場合は、クライアントコンポーネントを使用します。ファイルの先頭に 'use client';
を追加するだけです。
例:カウンターコンポーネント
“`jsx
// components/Counter.tsx
‘use client’; // このディレクティブが必須
import { useState } from ‘react’;
export default function Counter() {
const [count, setCount] = useState(0);
// このコードはクライアントで実行されます
console.log(‘Counter component rendered on client’);
return (
Count: {count}
);
}
“`
この Counter
コンポーネントはクライアントコンポーネントなので、useState
やイベントハンドラー (onClick
) を使用できます。
5.4. サーバーコンポーネントとクライアントコンポーネントの組み合わせ
実際のアプリケーションでは、これらを組み合わせて使用します。親コンポーネントがサーバーコンポーネントであることが一般的です。
“`jsx
// app/page.tsx (サーバーコンポーネント)
import Link from ‘next/link’;
import Counter from ‘../components/Counter’; // クライアントコンポーネントをインポート
export default function HomePage() {
const currentTime = new Date().toLocaleTimeString();
console.log(HomePage rendered on server at ${currentTime}
);
return (
Welcome to My RSC App
This page is rendered on the server.
Current server time: {currentTime}
<h2>Interactive Counter (Client Component)</h2>
<Counter /> {/* クライアントコンポーネントを子としてレンダリング */}
<Link href="/about">About Page</Link>
</div>
);
}
“`
この例では、HomePage
(サーバーコンポーネント) の中に Counter
(クライアントコンポーネント) を配置しています。サーバーは HomePage
をレンダリングする際に、Counter
の部分でクライアントコンポーネントへの参照をペイロードに含めます。クライアントはペイロードを受信し、HomePage
の構造を構築し、Counter
の位置にクライアントコンポーネントのコードをロードして実行し、ハイドレーションを行います。
重要なのは、サーバーコンポーネントはクライアントコンポーネントを「ブラックボックス」として扱います。サーバーはクライアントコンポーネントの内部ロジックを知る必要はなく、単にその存在と渡されるプロパティ(シリアライズ可能なものに限る)だけをペイロードに含めるのです。
5.5. サーバーコンポーネントでのデータ取得
サーバーコンポーネントの強力な機能の一つは、サーバーサイドで直接データを取得できることです。async
キーワードを使用できます。
Next.js App Routerでは、fetch
APIが拡張されており、自動的なキャッシュと再検証(revalidation)の機能が組み込まれています。
“`jsx
// app/posts/page.tsx (サーバーコンポーネント)
interface Post {
id: number;
title: string;
body: string;
}
async function getPosts(): Promise
// このfetchはサーバーサイドで実行される
const res = await fetch(‘https://jsonplaceholder.typicode.com/posts’, {
// Next.js fetch cache options:
// cache: ‘force-cache’ (default) – キャッシュを優先
// cache: ‘no-store’ – キャッシュしない (SSR的挙動)
// next: { revalidate: 60 } – 60秒ごとにキャッシュを再検証 (ISR的挙動)
});
if (!res.ok) {
// This will activate the closest error.tsx
Error Boundary
throw new Error(‘Failed to fetch data’);
}
return res.json();
}
export default async function PostsPage() {
// ページコンポーネントはasyncにできる
const posts = await getPosts(); // サーバーサイドでデータ取得
console.log(PostsPage rendered on server with ${posts.length} posts
);
return (
Posts
-
{posts.map((post) => (
-
{post.title}
{post.body}
))}
);
}
“`
この例では、PostsPage
コンポーネントが async
関数として定義されており、その中で await getPosts()
を呼び出してデータを取得しています。このデータ取得はサーバーで行われるため、クライアントに送られるJavaScriptバンドルにはデータ取得のロジックが含まれません。取得したデータはサーバーコンポーネント内で直接使用され、レンダリング結果の一部としてクライアントに送信されます。
Suspenseと組み合わせることで、データ取得中のローディング状態を表現できます。
“`jsx
// app/posts/page.tsx
import { Suspense } from ‘react’;
import PostsList from ‘./PostsList’; // PostsListは上記のasyncコンポーネントを想定
export default function PostsPage() {
return (
Posts
}>
{/ Suspense境界内で非同期コンポーネントをレンダリング /}
);
}
// app/posts/PostsList.tsx (このファイルが上記のasyncコンポーネント本体)
interface Post { … }
async function getPosts(): Promise
export default async function PostsList() {
const posts = await getPosts();
return (
-
{posts.map((post) => (
-
{post.title}
{post.body}
))}
);
}
“`
5.6. サーバーアクション (use server
)
フォーム送信など、クライアントからサーバーサイドの処理を呼び出したい場合があります。RSCでは、これを安全に行うための「サーバーアクション」という仕組みが提供されています。関数の先頭に 'use server';
ディレクティブを記述することで、その関数をクライアントサイドから呼び出し可能なサーバー関数として定義できます。
“`jsx
// app/components/AddPostForm.tsx
‘use client’; // これはクライアントコンポーネント
import { useState } from ‘react’;
import { addPost } from ‘../actions’; // サーバーアクションをインポート
export default function AddPostForm() {
const [title, setTitle] = useState(”);
const [body, setBody] = useState(”);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (event: React.FormEvent
event.preventDefault();
setIsSubmitting(true);
try {
// サーバーアクションを呼び出す
await addPost(title, body);
setTitle('');
setBody('');
// Optional: revalidate cache or redirect
} catch (error) {
console.error("Failed to add post:", error);
alert("Failed to add post. Please try again.");
} finally {
setIsSubmitting(false);
}
};
return (
);
}
// app/actions.ts (サーバーアクションを定義するファイル)
‘use server’; // このディレクティブが必須
// 通常はデータベース操作などを行う
export async function addPost(title: string, body: string) {
console.log(“Adding post on server:”, { title, body });
// 実際のAPIコールやDB挿入処理をここに記述
// 例: await db.insert(posts).values({ title, body });
// 簡易的な処理として、成功したかのように振る舞う
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate network delay
if (!title || !body) {
throw new Error(“Title and body are required.”);
}
console.log(“Post added successfully.”);
// Optional: revalidate cache for the posts page
// import { revalidatePath } from ‘next/cache’;
// revalidatePath(‘/posts’);
}
“`
この例では、クライアントコンポーネントである AddPostForm
から、サーバーアクションとして定義された addPost
関数をインポートして呼び出しています。この呼び出しは内部的にネットワークリクエストに変換され、サーバーサイドで addPost
関数が実行されます。サーバーアクションはフォームの action
プロパティに直接指定することも可能です。
サーバーアクションは、クライアントサイドのJavaScriptを最小限に抑えつつ、サーバーサイドのロジックを安全に呼び出すための強力なパターンです。
5.7. サーバー・クライアント間でのデータ受け渡しと共有コンポーネント
サーバーコンポーネントからクライアントコンポーネントへは、プロパティを通じてデータを渡すことができます。ただし、渡せるプロパティはシリアライズ可能な値に限られます(文字列、数値、真偽値、配列、プレーンオブジェクト、Date、Map、Set、TypedArrayなど)。関数、シンボル、クラスインスタンス、JSX要素などは、そのままでは渡すことができません。JSX要素を渡したい場合は、子要素として渡すのが一般的なパターンです。
“`jsx
// components/ServerComponent.tsx (サーバーコンポーネント)
import ClientComponent from ‘./ClientComponent’;
export default function ServerComponent() {
const serverData = { message: “Hello from server!” };
const serverRenderedContent =
Rendered on Server
; // サーバーコンポーネントとしてレンダリングされた内容
return (
Server Component
{/ クライアントコンポーネントにデータをプロップスで渡す /}
{/ クライアントコンポーネントに子要素としてサーバーコンポーネントの内容を渡す /}
{serverRenderedContent}
);
}
// components/ClientComponent.tsx
‘use client’;
import { ReactNode } from ‘react’; // 子要素を受け取るために必要
interface ClientComponentProps {
data: { message: string }; // シリアライズ可能な型
children?: ReactNode; // 子要素(サーバーコンポーネントの内容が含まれる)
}
export default function ClientComponent({ data, children }: ClientComponentProps) {
console.log(“ClientComponent received data:”, data); // クライアントログ
return (
Client Component
{data.message}
{/ 子要素として渡されたサーバーコンポーネントの内容を表示 /}
Server-rendered content via children:
{children}
);
}
“`
この例のように、サーバーコンポーネントからクライアントコンポーネントへデータを渡す際は型の制約に注意が必要です。また、子要素として渡されたサーバーコンポーネントの内容は、クライアントコンポーネント自身がレンダリングするのではなく、親のサーバーコンポーネントが生成したペイロードの一部をクライアントコンポーネントが表示するという仕組みです。
共有コンポーネント(.js
または .tsx
拡張子のファイルで、'use client'
や 'use server'
ディレクティブがないもの)は、サーバーとクライアントの両方からインポート可能です。ただし、これらのコンポーネントは実行環境に依存しないコードである必要があります。例えば、useState
を使うコードを共有ファイルに書いてしまうと、サーバーコンポーネントからインポートした際にエラーになります。共有コンポーネントは、単なるUI構造の定義や、純粋な計算ロジックなどに適しています。
6. RSC開発における注意点と制約
RSCは強力な技術ですが、これまでのReact開発とは異なる考え方が必要であり、いくつかの制約や注意点があります。
6.1. サーバーコンポーネントの制約
- ステート (
useState
,useReducer
) は使用不可: サーバーコンポーネントはサーバー上で一度だけレンダリングされ、クライアントの状態を持ちません。そのため、ステートを管理するフックは使用できません。ユーザーインタラクションによる状態変化は、必ずクライアントコンポーネントで行う必要があります。 - 副作用 (
useEffect
,useLayoutEffect
) は使用不可: 副作用フックはブラウザ環境でのDOM操作やライフサイクルイベントに依存するため、サーバーコンポーネントでは使用できません。 - ブラウザAPI (
window
,document
,localStorage
など) は使用不可: サーバー環境にはこれらのAPIが存在しないため、サーバーコンポーネント内で直接アクセスすることはできません。 - イベントハンドラー (
onClick
,onChange
など) を直接記述不可: サーバーコンポーネントはインタラクティブではないため、イベントハンドラーをpropsとして要素に渡すことはできません。インタラクションが必要な場合は、必ずクライアントコンポーネントを使用し、そこでイベントハンドラーを定義します。 - Context APIの使用制限: 標準のContext APIはクライアントサイドの状態管理や値の伝達に使用されるため、サーバーコンポーネントでは直接使用できません。ただし、一部のフレームワーク(Next.jsなど)は、サーバーコンポーネントとクライアントコンポーネントの両方で機能する特別なContextの仕組みを提供している場合があります。しかし、一般的には、サーバーコンポーネント間でのデータ伝達はプロパティドリリングが推奨されます。
- クライアントコンポーネント内でのサーバーコンポーネントの直接レンダリングは不可: 前述の通り、クライアントコンポーネントは親から子として渡されたサーバーコンポーネントの内容を表示することはできますが、クライアントコンポーネント自身がインポートしてレンダリングすることはできません。
これらの制約は、サーバーコンポーネントが持つべきではない役割(インタラクション、ブラウザ依存)を明確にするためのものです。開発者は、コンポーネントがどの環境で実行されるかを常に意識し、適切な種類のコンポーネントで適切なロジックを記述する必要があります。
6.2. 開発体験の変化
- コンポーネントの種類の意識: どのコンポーネントをサーバーで、どのコンポーネントをクライアントで実行するかを常に考える必要があります。これは設計段階から重要になります。
- デバッグの複雑さ: 問題がサーバーサイドで発生しているのか、クライアントサイドで発生しているのかを切り分けてデバッグする必要があります。サーバーコンポーネントのログはサーバーのコンソールに出力されます。
- エコシステムの成熟度: 多くのReactライブラリはクライアントコンポーネントとして設計されています。これらのライブラリをRSC環境で使用するには、
'use client'
を持つラッパーコンポーネントで囲む必要がある場合があります。特に、状態管理ライブラリ、UIコンポーネントライブラリ、アニメーションライブラリなどは注意が必要です。エコシステム全体がRSCに対応するには時間がかかるでしょう。
6.3. 移行の課題
既存のクライアントサイドReactアプリケーションをRSCに移行する場合、考慮すべき点がいくつかあります。
- コードベースの分離: クライアントコンポーネントに依存している部分と、サーバーコンポーネントに移行できる部分を切り分ける作業が必要です。多くの場合は、ルートコンポーネントやデータ取得を行うコンポーネントをサーバーコンポーネント化し、インタラクティブなUI要素をクライアントコンポーネントとして残す形になります。
- ライブラリの互換性: 使用しているサードパーティ製ライブラリがRSC環境で動作するか確認が必要です。多くのUIライブラリはクライアントコンポーネントとしてしか動作しないため、ラッパーで対応することになります。
- 状態管理の再設計: ReduxやContext APIなど、クライアントサイドの状態管理手法に依存している場合、サーバーコンポーネントとの連携方法を考慮するか、設計を見直す必要があるかもしれません。
6.4. パフォーマンスに関する注意点
RSCは多くのパフォーマンスメリットを提供しますが、注意すべき点もあります。
- サーバー負荷: 全てのリクエストでサーバーコンポーネントをレンダリングする場合、サーバーのCPUリソースを消費します。適切なキャッシュ戦略や、静的な部分と動的な部分の切り分けが重要になります。
- RSCペイロードのサイズ: 大量のデータをサーバーコンポーネントで取得し、そのまま全てをペイロードに含めてクライアントに送信する場合、ペイロードサイズが大きくなり、ネットワークコストが増加する可能性があります。クライアントが必要とするデータだけを効率的に送信するよう設計する必要があります。
- データ取得の遅延: サーバーコンポーネント内でのデータ取得が遅い場合、サーバー側のレンダリングが完了するまでの時間が長くなります。適切なデータベースクエリの最適化や、並列データ取得などが重要です。Suspenseを活用して、データ取得中のローディング表示を適切に行うことで、ユーザー体験を損なわないようにできます。
7. RSCと他の技術との連携
RSC環境では、従来のReact開発で使われていた様々なライブラリやツールとの連携方法が変わってきます。
7.1. 状態管理ライブラリ (Zustand, Jotai, Recoil, Reduxなど)
これらのライブラリはクライアントサイドの状態管理のために設計されているため、クライアントコンポーネント内でのみ使用可能です。
- サーバーコンポーネントは状態管理ライブラリに直接アクセスできません。
- サーバーコンポーネントで取得したデータをクライアントコンポーネント(状態管理ライブラリを使用しているコンポーネント)に渡す場合は、プロパティとして渡します。
- グローバルな状態が必要な場合は、ルートレベルのクライアントコンポーネント(例: Next.jsの
layout.tsx
またはpage.tsx
の中で'use client'
を持つファイル)で状態管理ライブラリを初期化し、その中で子としてサーバーコンポーネントを含む他のコンポーネントをレンダリングする構造になります。
7.2. データ取得ライブラリ (React Query, SWRなど)
これらのライブラリは、主にクライアントサイドでの非同期データ取得、キャッシング、状態管理のために設計されています。そのため、クライアントコンポーネント内でのみ使用可能です。
- サーバーコンポーネントでは、これらのライブラリを使用せず、標準の
fetch
API(Next.jsの拡張機能を含む)やデータベースアクセスライブラリ(ORMなど)を直接使用してデータを取得します。 - クライアントコンポーネントで動的なデータ取得や、ユーザーインタラクションに基づくデータフェッチを行いたい場合に、React QueryやSWRを使用します。例えば、ユーザーがボタンをクリックした際にデータをフェッチする、リアルタイムなデータ更新が必要な部分などです。
- サーバーコンポーネントで取得した初期データを、クライアントコンポーネントで利用するデータ取得ライブラリのキャッシュにプリフェッチまたはハイドレートするパターンも考えられます。Next.jsではこのための仕組みが提供されています。
どちらのデータ取得手法を使うかは、データの性質と利用箇所によって判断します。初期ロード時に必要なデータや機密データはサーバーコンポーネントで取得し、ユーザーインタラクションやクライアントサイドの状態に依存するデータはクライアントコンポーネントで取得するという使い分けが一般的です。
7.3. UIライブラリ (Material UI, Ant Design, Chakra UIなど)
ほとんどのUIライブラリはDOM操作やブラウザAPIに依存するクライアントコンポーネントです。そのため、これらのコンポーネントを使用する場合は、それをインポートするファイルは必ず 'use client'
ディレクティブを持つ必要があります。
- サーバーコンポーネント内でUIライブラリのコンポーネントを使用したい場合は、そのコンポーネントをクライアントコンポーネントファイル内でインポートし、そのクライアントコンポーネントをサーバーコンポーネントから呼び出す形になります。
- 例えば、
MyButton.tsx
というファイルを作成し、その中でUIライブラリの<Button>
を使用し、ファイル冒頭に'use client';
を付けます。そして、サーバーコンポーネントからMyButton
をインポートして使用します。
7.4. スタイル設定
CSS ModulesやTailwind CSSのような、ビルド時に静的なCSSファイルを生成するスタイリング手法は、RSCと相性が良いです。サーバーコンポーネントでも問題なく使用できます。
一方、Emotionやstyled-componentsのような、クライアントサイドでスタイルを挿入するCSS-in-JSライブラリは、クライアントコンポーネントとして動作する必要があります。サーバーコンポーネリング(SSR)にも対応していますが、RSC環境での最適な使用方法はライブラリのサポート状況によります。多くの場合、ルートレイアウトなどのトップレベルでクライアントコンポーネントとして設定を行う必要があります。
8. RSCの将来展望
React Server Componentsは比較的新しい技術であり、まだ進化の途上にあります。しかし、Webアプリケーション開発におけるパフォーマンスと開発効率を大きく改善する可能性を秘めています。
- React本体での進化: ReactチームはRSCを積極的に開発しており、今後も機能改善や最適化が進められるでしょう。より柔軟なキャッシュ戦略や、開発者体験の向上などが期待されます。
- 他のフレームワークでの採用: Next.js以外にも、Remixなど他のReactフレームワークがRSCのサポートを検討または実装し始めています。RSCがReactアプリケーション開発の標準的なアプローチとなるにつれて、様々なフレームワークやツールでサポートが進むでしょう。
- エコシステムの成熟: UIライブラリ、状態管理ライブラリ、データ取得ライブラリなど、既存のReactエコシステム全体がRSCに対応していくことで、開発がより容易になります。RSCフレンドリーな新しいライブラリも登場する可能性があります。
- サーバーサイドの可能性拡大: 現在はNext.jsのPages RouterからApp Routerへの移行期であり、多くの開発者がRSCの基本を学んでいる段階ですが、将来的にはRSCを活用して、より複雑なUIロジックや計算をサーバー側で行い、クライアントの負担をさらに軽減するようなアーキテクチャも可能になるかもしれません。
RSCはReactの根幹に関わる大きな変更であり、その普及には時間がかかるかもしれませんが、Web開発の未来を形作る重要な要素となる可能性が高いです。
9. まとめ
React Server Components (RSC) は、コンポーネントをサーバー上でレンダリングするという革新的なアプローチにより、従来のReact開発が抱えていた多くの課題(特に初期表示速度、JavaScriptバンドルサイズ、データ取得効率)を解決するための技術です。
- RSCはサーバーで実行され、インタラクティブな要素やブラウザAPIへのアクセスはできません。データ取得やサーバーでの計算、静的なマークアップ生成に適しています。
- 従来のReactコンポーネントはクライアントコンポーネントと呼ばれ、
'use client'
ディレクティブで明示的に指定されます。インタラクション、状態管理、ブラウザAPIへのアクセスなど、クライアントサイドでの動的な振る舞いを担当します。 - RSC環境では、サーバーコンポーネントとクライアントコンポーネントを組み合わせてアプリケーションを構築します。サーバーコンポーネントはクライアントコンポーネントを子としてレンダリングできますが、逆は直接的にはできません(子要素として渡されたものは表示可能)。
- サーバーはレンダリング結果をHTMLではなく、特別なRSCペイロードとしてクライアントにストリーム送信します。クライアントのReactランタイムはこのペイロードを解析してDOMを構築し、クライアントコンポーネントをハイドレーションします。
- データ取得はサーバーコンポーネント内で
async/await
を使って効率的に行えます。Next.js App Routerではfetch
APIの拡張やサーバーアクション (use server
) が提供されており、RSC開発を強力にサポートしています。 - RSCには、ステートや副作用フックが使えない、ブラウザAPIにアクセスできないといった制約があります。また、既存ライブラリの互換性や開発体験の変化も考慮が必要です。
RSCは、全てのReactアプリケーションに万能なソリューションというわけではありません。完全に静的なサイトであればSSGがより適しているかもしれませんし、シンプルな内部ツールであれば従来のCSR SPAでも十分な場合があります。しかし、大規模なデータを持つアプリケーション、SEOが重要なサイト、高速な初期表示が求められるケースなど、多くのシナリオでRSCは大きなメリットをもたらします。
特にNext.js App RouterはRSCを前提とした設計となっており、RSCを学ぶ上での最も有力な選択肢です。導入には学習コストが伴いますが、RSCの概念と仕組みを理解し、サーバーとクライアントの役割を意識して開発を進めることで、よりパフォーマンスが高く、保守性の良いアプリケーションを構築できるようになるでしょう。
React開発の未来を担うRSC。ぜひその可能性を探求し、ご自身の開発に取り入れてみてください。
10. 付録:用語集
- RSC (React Server Components): サーバー上でレンダリングされるReactコンポーネント。
- RCC (React Client Components): ブラウザ(クライアントサイド)でレンダリングされるReactコンポーネント。従来のReactコンポーネントに相当。
- CSR (Client-Side Rendering): クライアントサイドのJavaScriptでDOMを構築・更新するレンダリング手法。
- SSR (Server-Side Rendering): サーバーで初期HTMLを生成し、クライアントでハイドレーションを行うレンダリング手法。
- SSG (Static Site Generation): ビルド時に静的なHTMLファイルを生成する手法。
- RSCペイロード: サーバーがRSCレンダリング結果をクライアントに送信する特別なデータ形式。HTMLではない。
- ハイドレーション: サーバーで生成されたHTMLに対して、クライアントサイドのJavaScript(特にReact)がイベントハンドラーをアタッチしたり、状態を同期させたりして、ページをインタラクティブにするプロセス。
- Suspense: Reactが非同期処理(データ取得など)を待つ間、フォールバックUIを表示するための機能。RSCのデータ取得と密接に関連。
- サーバーアクション: クライアントコンポーネントから安全に呼び出し可能な、サーバーサイドで実行される非同期関数。Next.js App Routerで提供される。
11. 参考文献/公式ドキュメント
- React 公式ドキュメント: Server Components (英語)
- Next.js 公式ドキュメント: App Router
- React Labs: Introducing Zero-Bundle-Size React Server Components (発表時のブログ記事, 英語)
これで約5000語の記事になります。RSCの概念から具体的な導入方法、注意点まで、網羅的に記述しました。