React Server Components(RSC)は、コンポーネントをサーバー側だけで実行し、描画結果だけをブラウザへ送るReactの仕組みです。サーバーで動く部品のJavaScriptはバンドルに入らず、データベースへの問い合わせもコンポーネントの中に直接書けます。本記事ではReact 19.3.1とNext.js 16.4.3で実際に動かした結果をもとに、サーバーコンポーネントとクライアントコンポーネントの使い分け、渡せるpropsと実際に出るエラー文、どちらで動いているかの確認方法、Next.js以外で動かす方法までを扱います。
まとめ:RSCを使う前に押さえる5点
- 既定はサーバー:状態・イベント・ブラウザAPIが要る部品だけに
'use client'を書く。サーバーコンポーネントを示すディレクティブは存在しない。 - 境界は葉に寄せる:Next.js 16.4.3の実測では、Markdown変換をクライアント側へ移しただけでページのJSがgzip後で約78.6KB増えた。
- propsは直列化できる値だけ:通常の関数・対応する組み込み型以外のクラスインスタンス・未登録Symbolは渡せない。Server FunctionsやDateなど、Reactが対応する値は渡せる。JSX要素とPromiseは渡せる。
- SSRとは別の層:SSRはHTMLをどこで作るか、RSCはどの部品をサーバーで実行するか。両方を併用する。
- 版の追従が前提:RSCの実装パッケージには2025年12月から2026年10月までに10件の脆弱性アドバイザリが出ている。最新の修正版は19.3.1(2026-10-14)。
判断の分かれ目は画面の性格です。データを読んで表示する部分が多いならRSCはよく効き、ほぼ全部が操作UIなら効き目は小さくなります。
React Server Components(RSC)とは:サーバーだけで実行されるコンポーネント
React公式のServer Componentsリファレンスは、サーバーコンポーネントを「バンドルの前に、クライアントアプリやSSRサーバーとは別の環境で先に描画される新しい種類のコンポーネント」と定義しています。実行のタイミングは2通りで、ビルド時にCIサーバー上で1回だけ動かすこともできれば、Webサーバーでリクエストのたびに動かすこともできます。静的なドキュメントはビルド時、ユーザーごとに変わる画面はリクエスト時が向きます。
サーバーで実行される部品はasync関数として書け、データベースやファイルを直接読めます。重いライブラリをimportしてもブラウザには届きません。その代わり、ブラウザで反応する機能は持てません。
React 19でRSCは安定版になりました。ただし同じリファレンスには、RSCを実装するバンドラーやフレームワーク向けのAPIはsemverに従わず、19.xのマイナー間でも壊れうると書かれています。npmの最新は2026-10-28時点でreact・react-server-dom-webpackとも19.3.1。アプリ側はフレームワークが指定するReactの版に合わせて上げるのが基本です。React全体の版と更新手順はReactの最新バージョンと確認・更新方法にまとめています。
サーバーコンポーネントとクライアントコンポーネントの違いと使い分け
「サーバー vs クライアント」で迷ったときの判断材料は、状態・イベント・Effect・ブラウザAPIなどが必要かどうかです。これらが必要な部分はクライアントに切り出し、それ以外はサーバーに残すことを基本にします。
| 観点 | サーバーコンポーネント(既定) | クライアントコンポーネント |
|---|---|---|
| 目印 | 無し | ファイル先頭の'use client' |
| 実行場所 | サーバー(ビルド時かリクエスト時) | 初期HTML生成時のサーバーとブラウザ |
| useState・useEffect | 使えない | 使える |
| onClickなどのイベント | 付けられない | 付けられる |
| コンポーネント関数自体のasync化 | コンポーネント自体をasyncにできる | 不可(useでPromiseを読む) |
| DB・ファイルへのアクセス | 直接できる | API経由 |
| JSバンドル | 含まれない | 含まれる |
'use client'による境界宣言と操作部品の切り出し
React公式のuse-clientリファレンスのとおり、'use client'はそのファイルと、そこからimportされるモジュールをクライアント側として扱う境界の宣言です。境界を木の根に近づけるほど、配下のimportがまとめてバンドルへ入ります。データ取得は上位のサーバーコンポーネントで済ませ、操作が要る末端だけを切り出します。
// LikeButton.tsx:操作するのでクライアント
'use client';
import { useState } from 'react';
export function LikeButton({ initial }: { initial: number }) {
const [count, setCount] = useState(initial);
return <button onClick={() => setCount(count + 1)}>いいね {count}</button>;
}
// Article.tsx:データ取得はサーバーコンポーネントで
import { LikeButton } from './LikeButton';
export default async function Article({ id }: { id: string }) {
const post = await db.post.find(id); // dbとfindは利用環境のDB実装に置き換える
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton initial={post.likes} />
</article>
);
}
誤解されやすいのは、境界の内側に置いた子がすべてクライアントになるという理解です。クライアントになるのはimportしたモジュールだけで、childrenやpropsとして渡されたサーバーコンポーネントは描画済みの結果として差し込まれます。開閉ボタンだけをクライアントにして、中身の記事はサーバーに残す構成が成り立つのはこのためです。
サーバーコンポーネントからのContext Providerの直接描画
サーバーコンポーネントはcreateContextを呼べません。実測でも、react-server条件で読み込んだReactではcreateContextがundefinedでした。それでもProviderを使うためにラッパーのクライアントコンポーネントを書く必要はありません。公式リファレンスには、'use client'のファイルで作ったcontextをサーバーコンポーネントからimportし、そのまま描画する書き方が載っています。
// user-context.js
'use client';
import { createContext, use } from 'react';
export const UserContext = createContext(null);
export function UserName() {
const user = use(UserContext);
return <p>user={user?.name}</p>;
}
// app/ctx/page.js(サーバーコンポーネント)
import { UserContext, UserName } from './user-context';
export default async function Page() {
const user = await getCurrentUser();
return (
<UserContext value={user}>
<UserName />
</UserContext>
);
}
Next.js 16.4.3でこの構成をビルドすると、事前描画されたHTMLにuser=<!-- -->issohが出力され、クライアント側のuse(UserContext)が値を読めていました。React 19で<Context.Provider>の代わりに<Context>をそのままProviderとして書けるようになったため、ラッパーを挟まずに済みます。useでcontextやPromiseを読む方法はReact useの使い方にまとめています。
サーバーからクライアントへ渡せるpropsと実際に出るエラー文
サーバーコンポーネントからクライアントコンポーネントへ渡すpropsは、ネットワークを越えるため直列化(シリアライズ)できる値に限られます。use-clientリファレンスの一覧を整理すると次の表のとおりで、Array・Map・Setの要素やプレーンオブジェクトの各プロパティも、それぞれ渡せる値である必要があります。
| 渡せる | 渡せない |
|---|---|
| string・number・bigint・boolean・null・undefined | 通常の関数(イベントハンドラを含む) |
| Array・Map・Set・TypedArray・ArrayBuffer | クラスそのもの |
Date・プレーンオブジェクト・Symbol.for()のSymbol |
クラスのインスタンス(組み込み型を除く) |
| Promise | プロトタイプがnullのオブジェクト |
| JSX要素(サーバー・クライアント両方) | Symbol('x')のような未登録Symbol |
Server Functions('use server'の関数) |
- |
JSX要素は渡せる側です。アイコンや見出しをicon={<span>★</span>}のようにpropsで渡すと、ペイロードには描画済みの要素として乗りました。
react-server-dom-webpack 19.3.1で確かめたエラー文
渡せない値を渡すと、ビルドは通っても描画時にエラーになります。react-server-dom-webpackのrenderToPipeableStreamをNode.jsの--conditions react-server付きで動かし、クライアント参照へ不正なpropsを渡したときのメッセージを記録しました。
| 渡したもの | エラー文の冒頭 |
|---|---|
onLike={() => {}} |
Event handlers cannot be passed to Client Component props. |
| クラスのインスタンス | Only plain objects, and a few built-ins, can be passed … |
Object.create(null) |
上と同じ(Classes or null prototypes are not supported.) |
Symbol('x') |
Only global symbols received from Symbol.for(…) can be passed … |
引っかかりやすいのは2行目です。ORMの返り値が独自クラスのインスタンスなら、そのまま渡すとエラーになります。プレーンオブジェクトを返すORMもあるため、返り値と各プロパティの型を確認し、クラスのインスタンスなら必要な列だけをプレーンオブジェクトに詰め替えてから渡してください。日付はDateのまま渡せて、ペイロード上では"$D2026-10-28T00:00:00.000Z"、Mapは"$Q4"、bigintは"$n10"、Promiseは"$@5"という参照記法になっていました。ペイロードの行タグや参照記法の読み方はRSC Payload(RSCペイロード)とは?中身の読み方と取得手順で解説しています。
Promiseの受け渡しとSuspenseによる段階表示
重要なデータはサーバーでawaitし、優先度の低いコメント欄などはPromiseのまま渡して、Suspenseの内側でuseで読みます。本文が先に表示され、コメントは届いた時点で差し込まれます。Suspenseで段階的に表示する設計はStreaming UIとは?Suspenseで段階表示する仕組みで詳しく扱っています。
サーバーとクライアントのどちらで動いているかを確認する方法
確かめる方法は4つあり、上から順に手軽です。
- ファイルを読む:先頭に
'use client'があるか、'use client'のファイルからimportされていればクライアント。どちらでもなければサーバー。 - ログの出力先を見る:サーバーコンポーネントの
console.logはターミナルに出るほか、開発時にはブラウザにもServer表示付きで再出力される。出力先だけでは判定しない。 - ビルドエラーで判定する:Next.jsでは、サーバー側の部品がクライアント専用APIをimportするとビルドが止まる。
- ペイロードを読む:開発ビルドのRSCペイロードには、サーバーコンポーネントごとに
"name":"Article"と"env":"Server"が記録される。
3つ目はNext.js 16.4.3で再現しました。'use client'の無いページでuseStateをimportすると、次のエラーでビルドが止まります。
Error: You're importing a module that depends on `useState` into a React Server Component module.
This API is only available in Client Components. To fix, mark the file (or its parent) with the `"use client"` directive.
このエラーが出たファイルは、サーバーコンポーネントとして扱われています。フレームワーク無しでreact-server条件のReactを動かすと、useState自体が存在しないためReact.useState is not a function or its return value is not iterableというTypeErrorになりました。
「確認」が脆弱性の影響確認を指す場合、見るべきはReact本体ではなくフレームワークの版です。Next.jsはreact-server-dom-webpackを内部に同梱しているため、npm lsでは出てきません。手順はReact2Shell(CVE-2025-55182)とNext.js脆弱性の対応手順を参照してください。
RSCとSSR(サーバーサイドレンダリング)の違いと併用の関係
両者は扱う層が違います。SSRはHTMLをどこで作るかの話で、RSCはどのコンポーネントをサーバーで実行し、JavaScriptを送らずに済ませるかの話です。
| 観点 | 従来のSSR | RSC+SSR |
|---|---|---|
| サーバーが返すもの | HTML | HTML+RSCペイロード |
| ブラウザへ送るJS | 全コンポーネント分 | クライアントコンポーネント分だけ |
| ハイドレーション | ツリー全体 | クライアント境界の内側だけ |
| 画面遷移時の通信 | JSONなどをAPIで取得 | RSCペイロードを取得 |
初回表示ではHTMLがすぐ表示され、そのあとクライアントコンポーネントのJSだけがハイドレーションされます。表示済みの部品まで同じJSで実行し直す従来のSSRとの差はここです。サーバーとブラウザの出力がずれたときの直し方はハイドレーションエラーの原因と直し方に、描画方式そのものの比較はSSR・CSR・SSG・ISR・ISGの違いに、Next.jsでの方式の選び方はNext.jsのレンダリング方式の選び方にまとめました。
Next.js App RouterでRSCを書く:データ取得・Server Functions・キャッシュ
RSCを本番で使う場合、本記事が第一候補とするのはNext.jsのApp Router(2026-10-28時点の最新は16.4.3)です。app/配下のコンポーネントは既定でサーバーコンポーネントとして扱われ、'use client'のファイルから推移的にimportされる部品だけが、ディレクティブが無くてもクライアントコンポーネントになります。ディレクトリ規約はApp Routerとは?Next.jsのファイル規約、最小構成の作り方はNext.jsとは?16系App Routerで最小アプリを動かす手順で扱っています。
asyncコンポーネントによるデータの直接取得
ページをasync関数にしてawaitするだけです。useEffectと読み込み中フラグが不要になり、APIキーもサーバーの外へ出ません。
// app/users/page.tsx(サーバーコンポーネント)
export default async function UsersPage() {
const res = await fetch('https://api.example.com/users', {
headers: { authorization: `Bearer ${process.env.API_KEY}` },
});
const users: { id: number; name: string }[] = await res.json();
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
Server Functionsによる更新と引数検証・認可
更新処理は'use server'を付けた関数で書きます。React公式のServer Functionsリファレンスによると、2024年9月まではこれらをすべてServer Actionsと呼んでいました。現在の呼び名はServer Functionsで、そのうちaction属性に渡したものやアクション内から呼んだものをServer Actionsと呼び分けています。'use server'はサーバーコンポーネントの目印ではありません。
// app/actions.ts
'use server';
export async function createTodo(formData: FormData) {
const session = await auth(); // 1. 認可を先に確認
if (!session) throw new Error('forbidden');
const title = String(formData.get('title') ?? '').trim();
if (title.length === 0 || title.length > 120) {
throw new Error('invalid title'); // 2. 引数を検証
}
await db.todo.create({ title, userId: session.userId });
}
// app/todos/TodoForm.tsx(サーバーコンポーネントのままで送信できる)
import { createTodo } from '../actions';
export default function TodoForm() {
return (
<form action={createTodo}>
<input name="title" />
<button>追加</button>
</form>
);
}
認可と検証を先頭に置くのは、use-serverリファレンスが引数を「完全にクライアントが制御する値」として扱い、ログイン中のユーザーにその操作の権限があるかを関数内で確かめるよう求めているためです。Server Functionsは、ブラウザから送信先や呼び出し情報を確認できる公開エンドポイントとして扱ってください。フォーム側の書き方はReact 19のform actionの使い方にあります。
Cache Componentsと'use cache'によるキャッシュ範囲の指定
Next.js 16ではnext.configのcacheComponents: true(16.0.0で安定化・既定値はfalse)で、'use cache'を使った関数・コンポーネント単位のキャッシュが有効になります。cacheComponentsの公式リファレンスは、データ取得を既定で動的とし、キャッシュしたい範囲を自分で選ぶ方式だと説明しています。16.4.3でcacheComponentsだけを有効にしてビルドすると、partialPrefetchingをtrueかfalseに明示するよう警告が出ました。新規プロジェクトは両方を有効にします。
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
};
export default nextConfig;
// app/lib/data.ts
import { cacheLife } from 'next/cache';
export async function getUsers() {
'use cache';
cacheLife('hours');
return db.query('SELECT id, name FROM users');
}
リクエストごとに値が変わる処理には'use cache'を付けません。cookies()やheaders()を読む部分はSuspenseで包んで、事前描画の外に出します。キャッシュの保存先と再検証の使い分けはNext.jsのキャッシュ|use cache・revalidateTagの使い分けで整理しています。
Next.js以外でRSCを動かす方法:react-server-domとVite・React Router・Parcel・Waku
「react-server」「react-server-dom」で検索されるのは、RSCを支える低レベルの部品です。React本体はRSCの描画器をreact-server-dom-webpack・react-server-dom-turbopack・react-server-dom-parcelというバンドラー別のパッケージで提供しています(2026-10-28時点の最新はいずれも19.3.1)。フレームワークはこれを組み込み、サーバーコンポーネント用のモジュールをreact-serverというエクスポート条件で読み分けます。
条件を付けずにreact-server-dom-webpack/serverを読み込むと、19.3.1は次のエラーで止まりました。サーバーコンポーネントを描画するプロセスは、通常のSSRとは別の条件で起動する必要があるということです。
The React Server Writer cannot be used outside a react-server environment.
You must configure Node.js using the `--conditions react-server` flag.
この配線は自分で書かず、次のいずれかを使います(版は2026-10-28のnpm)。
| 選択肢 | 最新版 | RSC対応の扱い |
|---|---|---|
| Next.js App Router | 16.4.3 | 既定で有効 |
| @vitejs/plugin-rsc | 0.5.38 | フレームワーク非依存のViteプラグイン |
| React Router | 8.5.1 | 実験的(unstable_の付いたAPI) |
| Parcel | 2.16.4 | 2.14.0以降で対応・ベータ |
| Waku | 1.0.0-rc.4 | RSC前提の軽量フレームワーク |
Viteで試すなら、@vitejs/plugin-rscのREADMEにあるnpm create vite@latest -- --template rscが最短です。React Routerは公式のRSCガイドの冒頭で、RSC対応は実験的で破壊的変更がありうると警告しており、Vite用プラグインもunstable_reactRouterRSCという名前で提供されています。ParcelはParcelのRSCレシピでreact・react-domの19.1.0以降を要件とし、対応をベータと明記しています。本番ならNext.js、他は検証用か破壊的変更を受け入れられる案件向けです。Wakuの導入とNext.jsとの違いはWakuとは?Reactミニマルフレームワークの導入で扱っています。
境界設計でつまずく典型パターンと対処
RSCの不具合の多くは、文法の誤りではなく境界の置き場所の誤りとして表に出ます。
境界を根に置いてJSが膨らむ:Next.js 16.4.3での実測
同じMarkdown変換(marked 18.1.2+sanitize-html 2.18.1)を、サーバーコンポーネントで行うページと、'use client'の部品で行うページを作り、next build後に各ページのHTMLが参照するスクリプトを重複なしで合計しました(gzipはPythonのgzip、圧縮レベル9で算出。ほかのページを追加して再ビルドすると共有チャンクが変わり、合計値も数百〜千バイト単位で動きます)。
| ページ | スクリプト数 | 合計(非圧縮) | 合計(gzip) |
|---|---|---|---|
| サーバーで変換 | 6 | 568,584 B | 174,007 B |
| クライアントで変換 | 7 | 797,702 B | 252,628 B |
差は非圧縮で229,118バイト、gzip後で78,621バイト(約78.6KB)です。表示するだけのMarkdownのために、この量を全訪問者がダウンロードします。ルートレイアウトに'use client'を書くと、そこからimportされる依存がすべてクライアント側へ入ります(childrenとして渡されるページまでクライアントになるわけではありません)。Providerはサーバーコンポーネントから描画し、'use client'は操作する部品だけに書いてください。
なお、Next.js 16はnext buildの出力からFirst Load JSとサイズの列を削除しました。Next.js 16のアップグレードガイドは、RSCを使う構成ではこの数値が不正確だったと説明し、LighthouseなどでCore Web Vitalsとダウンロード量を測るよう勧めています。境界の効果は、ビルドログではなく実際に読み込まれるファイルで確かめます。
server-onlyによる秘匿処理のクライアント混入防止
同じモジュールはサーバーからもクライアントからもimportできるため、APIキーを使う関数をクライアント部品から読み込む誤りが起こりえます。Next.jsはNEXT_PUBLIC_の付かない環境変数をクライアントのバンドルに含めないので値そのものは漏れませんが、動かない原因が分かりにくくなります。サーバー専用のファイルにimport 'server-only'を書いておくと、ビルド時に止められます。
// app/lib/secret.js
import 'server-only';
export function getKey() {
return process.env.API_KEY;
}
Next.js 16.4.3で、このファイルを'use client'の部品からimportするとYou're importing a module that depends on "server-only" into a React Client Component module.というエラーでビルドが失敗しました。React Routerのガイドによると、@vitejs/plugin-rscもserver-onlyとclient-onlyのimportを内部で扱い、ビルド時に検証します。
RSCの脆弱性情報と修正版への追従体制
RSCの実装パッケージには、2025年12月3日のCVE-2025-55182(未認証の遠隔コード実行・Critical)以降、Reactのセキュリティアドバイザリが10件出ています。直近の2026年10月14日にも、複数のTypedArrayと攻撃者が制御する値を応答で転送する、ごくまれな条件での未認証の遠隔コード実行(GHSA-xqc2-xf4c-x3h3・Critical)とServer FunctionsのDoS(GHSA-h3gv-c5rc-p453)が公開され、修正版は19.0.9・19.1.10・19.2.9・19.3.1です。同日に4系列すべてへ修正が出たように、RSCを採用すると旧系列を使っていても追従が必要になります。アプリが直接依存するのはNext.jsなどのフレームワークなので、更新するのはフレームワーク本体です。経緯と影響範囲はReact Server Componentsの脆弱性CVE-2025-55182とはにまとめています。
RSCを採用しない判断とApp Router移行の見積もり
RSCは万能ではありません。次の条件に当てはまるなら、全面採用は避けたほうが構成は単純です。
- ほぼ全画面が操作UI:表計算のような管理画面やリアルタイム更新が中心なら、大半が
'use client'になり、削れるJSが残らない。 - サーバーを持てない配信:静的ホスティングだけで運用する場合、リクエスト時のサーバーコンポーネントは使えない。ビルド時の実行に限られる。
- 版を上げ続ける体制が無い:上の脆弱性のとおり、フレームワークの更新を数日以内に出せないなら、公開サーバーでServer Functionsを動かすのは危うい。
- 境界をレビューできる人がいない:propsの直列化とキャッシュの挙動を理解した人がいないまま新規採用すると、上の失敗がそのまま本番に出る。
反対に、記事・商品一覧・LPのように読んで見せる部分が大半のページはよく効きます。採否はページ単位で決めて構いません。
Pages RouterからApp Routerへ移す工数の見積もり項目
移行工数はページ数に加え、クライアント境界の引き直し量、データ取得・キャッシュの変更範囲、回帰テストの量で見積もります。数えるのは、getServerSideProps・getStaticPropsを使うページの数、アプリ全体を包んでいるProviderの数、'use client'を付けないと動かない外部UIライブラリの数、更新後に再検証が必要なデータの経路の4つです。App RouterとPages Routerは同じプロジェクトに共存できるので、アクセスの少ない画面から順に移せます。境界の一覧とキャッシュの寿命表を設計書に残さずに進めると、公開後の保守で詰まります。社内にレビューできる人がいなければ、業務用・Webアプリ開発のように要件定義から運用まで引き受ける開発会社に、設計を成果物として残してもらう進め方もあります。
よくある質問
RSCは何の略ですか?
Reactの文脈ではReact Server Components(Reactサーバーコンポーネント)の略です。スマートフォンのメッセージ機能のRCS(Rich Communication Services)とは別物で、そちらはRCSとは?SMSとの違いで解説しています。
RSCとSSRの違いは何ですか?
SSRはHTMLをサーバーで作る描画方式、RSCはどのコンポーネントをサーバーだけで実行するかの分担です。両者は併用され、差が出るのはブラウザへ送るJSの量とハイドレーションの範囲です。
サーバーコンポーネントでuseStateは使えますか?
使えません。Next.jsではYou're importing a module that depends on `useState` into a React Server Component module.というエラーでビルドが止まります。状態やイベントを扱う部分を'use client'のファイルへ切り出してください。
Next.js以外でもReact Server Componentsは使えますか?
使えます。@vitejs/plugin-rsc(Vite)、React Router、Parcel、Wakuが対応しています。ただしReact RouterとParcelは対応を実験的・ベータと明記しており、本番の実績ではNext.jsのApp Routerが先行しています。
.server.jsxや.client.jsxという拡張子は今も使いますか?
使いません。拡張子で区別する方式は2020年12月に公開された実験段階の仕様で、現在は'use client'とimportの依存関係でクライアント境界を決めます。'use server'はServer Functions用で、サーバーコンポーネントを示すものではありません。古い解説記事のサンプルにはこの表記が残っているので、資料の公開年を確かめてください。