Server Componentsとは

React Server Components(RSC)は、サーバーでのみレンダリングされ、クライアントバンドルに一切含まれない新しい種類のコンポーネントです。 2020年12月にMetaのReactチームがRFCを発表し、Next.js 13のApp Routerで実用化が進み、React 19で正式にReact本体の機能として安定版となりました。

従来のReactでは、すべてのコンポーネントがクライアントサイドのJavaScriptバンドルに含まれていました。 サーバーサイドレンダリング(SSR)を使っても、HTMLを生成した後にクライアント側で再度同じコンポーネントを実行して「ハイドレーション」する必要がありました。 Server Componentsはこの構造を根本から変えます。サーバーで実行された結果だけがクライアントに送られ、そのコンポーネントのJavaScriptコードは一切ブラウザに到達しません

Server Component vs Client Component

React 19以降、コンポーネントはServer ComponentClient Componentの2種類に分類されます。 ファイル先頭にディレクティブがない場合はデフォルトでServer Componentとして扱われます。

能力 Server Component Client Component
async/awaitで直接データフェッチ 可能 不可(useEffect等が必要)
useState / useReducer 不可 可能
useEffect 不可 可能
イベントハンドラ(onClick等) 不可 可能
ブラウザAPI(localStorage等) 不可 可能
DB・ファイルシステムへの直接アクセス 可能 不可
JSバンドルサイズへの影響 なし(送信されない) あり(バンドルに含まれる)
レンダリング環境 サーバーのみ サーバー(SSR)+ クライアント

重要なのは、Server Componentが「SSRの進化版」ではないという点です。 SSRはClient Componentをサーバーでも実行してHTMLを生成する仕組みですが、ハイドレーションのためにクライアントでも同じコードが必要です。 Server Componentはそもそもクライアントで実行されることを前提としていないため、バンドルサイズを劇的に削減できます。

React Flight Protocol — ワイヤーフォーマット

Server Componentのレンダリング結果をクライアントに送信するために、ReactはFlight Protocolと呼ばれる独自のワイヤーフォーマットを使用します。 これはHTMLではなく、行ベースのテキスト形式でReact要素ツリーをシリアライズしたものです。

行タイプと構造

Flight Protocolの各行は、行ID・タイプ接頭辞・ペイロードで構成されます。主要な行タイプは以下の通りです。

// M行: モジュール参照(Client Componentの場所を示す)
M1:{"id":"./src/components/Counter.js","name":"Counter","chunks":["chunk-abc"]}

// J行: React要素ツリー(JSON形式のvDOM記述)
0:["$","div",null,{"children":[["$","h1",null,{"children":"Hello"}],"$L1"]}]

// S行: Symbol(React.Suspense等の特殊要素)
S1:"react.suspense"

// H行: ヒント(プリロードすべきリソース)
H1:["prefetchDNS","https://cdn.example.com"]

$参照システム

Flight Protocolでは、$から始まる特殊な参照がツリー内のさまざまな要素を指し示します。

  • $ — React要素(createElement相当)のマーカー
  • $L<ID> — 遅延読み込みされるClient Componentへの参照(Lazy Reference)
  • @<ID> — 既に送信済みの行への後方参照
  • $RE — React要素を示す特殊シンボル

たとえば "$L1" は「M1行で定義されたClient Componentをここに挿入せよ」という意味になります。 クライアントのReactランタイムがこのペイロードを解釈し、Client Componentの実際のコードを動的にインポートしてツリーに埋め込みます。

なぜHTMLではなくカスタムプロトコルなのか

HTMLを送信するSSRとは異なり、Flight Protocolが採用された理由は主に3つあります。

  • クライアント状態の保持: HTMLを差し替えると入力中のフォーム値やフォーカス状態が失われるが、Flight形式ならReactツリーを差分更新できるため状態が維持される
  • ストリーミング対応: 行ベース形式のため、Suspense境界ごとに段階的にデータを送信でき、ブラウザは到着した分から逐次レンダリングできる
  • 型の保持: Date、Map、Set、BigIntなどJavaScriptの型をHTMLより正確にシリアライズでき、Client Component側で型情報が失われない
sequenceDiagram
  participant B as ブラウザ
  participant S as サーバー
  participant DB as データベース

  B->>S: リクエスト
  S->>DB: データ取得
  DB-->>S: データ返却
  Note over S: Server Component を実行
  Note over S: Flight Payload を生成
  S-->>B: Flight ストリーム開始
  Note over B: 行を逐次パース
  Note over B: Server部分を即座にレンダリング
  Note over B: Client Component を動的インポート
  Note over B: ハイドレーション(Client部分のみ)
RSCリクエストフロー: サーバーでコンポーネントを実行し、Flight形式でストリーミング送信

Server/Client境界の仕組み

Server ComponentとClient Componentの境界は、ディレクティブと呼ばれるファイル先頭の宣言によって定義されます。

"use client" ディレクティブ

ファイルの先頭に "use client" と記述すると、そのファイルとそこからインポートされるすべてのモジュールはClient Componentとして扱われます。 このディレクティブはエントリポイントを定義するものであり、「このファイルがServer/Client境界の境目である」という宣言です。

"use client";

import { useState } from "react";

// このコンポーネントはClient Component
// useState等のHooksやイベントハンドラが使える
export function Counter({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      カウント: {count}
    </button>
  );
}

"use server" ディレクティブ

"use server" はServer Functions(Server Actions)を定義するためのディレクティブです。 ファイルの先頭、または関数の先頭に記述できます。 マークされた関数はサーバーでのみ実行され、クライアントからはRPCのように呼び出せます。

// ファイル先頭に書く場合 — ファイル内のすべてのexport関数がServer Function
"use server";

export async function createTodo(formData: FormData) {
  const title = formData.get("title") as string;
  await db.todo.create({ data: { title } });
}

// 関数内に書く場合 — その関数だけがServer Function
async function submitForm() {
  "use server";
  // サーバーで実行される処理
}

Client Referenceとシリアライズ制約

Server ComponentからClient Componentをレンダリングする際、サーバーは実際のコンポーネントコードではなくClient Referenceと呼ばれる参照オブジェクトを生成します。 これはモジュールのパスとエクスポート名を含む軽量なメタデータであり、Flight Payloadに埋め込まれます。

Server ComponentからClient ComponentにPropsとして渡せるデータには制約があります。 シリアライズ可能な値のみが境界を越えられます。

  • 渡せる: 文字列、数値、boolean、null、配列、プレーンオブジェクト、Date、Map、Set、FormData、JSX要素
  • 渡せない: 関数、クラスインスタンス、Symbol(Server Actionsとして定義された関数は例外的に渡せる)

Server Actions — 型安全なRPC

Server Actions(正式名称: Server Functions)は、クライアントからサーバーの関数を直接呼び出せる仕組みです。 フォームの action 属性に関数を渡すだけで、送信時にサーバーで処理が実行されます。

フォームとの統合

// Server Component
import { createTodo } from "./actions";

export default function TodoForm() {
  return (
    <form action={createTodo}>
      <input type="text" name="title" required />
      <button type="submit">追加</button>
    </form>
  );
}

// actions.ts
"use server";

export async function createTodo(formData: FormData) {
  const title = formData.get("title") as string;
  // サーバーでDB操作
  await db.todo.create({ data: { title } });
  // 必要に応じてrevalidatePathでキャッシュ無効化
}

プログレッシブエンハンスメント

Server Actionsの最も革新的な特徴は、JavaScriptが無効な環境でも動作することです。 <form action={serverAction}> はHTMLのネイティブフォーム送信として機能するため、 JSが読み込まれる前でも、あるいはJSが無効化されていても、フォーム送信が可能です。 JSが有効な場合は、ページ遷移なしの非同期送信に自動的にエンハンスされます。

useActionState + useFormStatus + useOptimistic

React 19では、Server Actionsと連携する3つのHooksが導入されました。

"use client";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { useOptimistic } from "react";
import { addItem } from "./actions";

// useFormStatus: フォームの送信状態を取得
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "送信中..." : "追加"}</button>;
}

// useActionState: アクションの状態管理(エラー、前回の結果等)
function TodoForm() {
  const [state, formAction, isPending] = useActionState(addItem, { error: null });
  return (
    <form action={formAction}>
      <input name="title" />
      {state.error && <p style={{ color: "red" }}>{state.error}</p>}
      <SubmitButton />
    </form>
  );
}

// useOptimistic: サーバー応答前にUIを先行更新
function TodoList({ todos }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (current, newTodo) => [...current, { ...newTodo, pending: true }]
  );
  // ...
}

ストリーミングSSR

React 18で導入されたストリーミングSSRは、Server Componentsと組み合わせることで真価を発揮します。 従来のSSRは全体のHTMLが完成するまでレスポンスを返せませんでしたが、ストリーミングSSRではSuspense境界ごとに段階的にHTMLを配信できます。

Suspense境界での段階的配信

// Suspenseでストリーミング境界を定義
export default function Page() {
  return (
    <main>
      <h1>ダッシュボード</h1>
      {/* ナビゲーションは即座に送信 */}
      <Navigation />

      {/* データ取得中はフォールバックを先に送信 */}
      <Suspense fallback={<LoadingSkeleton />}>
        <SlowDataComponent />
      </Suspense>
    </main>
  );
}

この仕組みの流れは以下の通りです。

  1. サーバーはSuspense境界外のHTMLを即座に送信開始する
  2. Suspense内のコンポーネントがデータ取得中の場合、フォールバック(LoadingSkeleton)のHTMLを代わりに送信する
  3. データ取得が完了すると、後続のHTMLチャンクとして実際のコンテンツを送信する
  4. ブラウザ側のインラインスクリプトが、フォールバック部分を実際のコンテンツにDOM操作で置換する

Selective Hydrationとの連携

ストリーミングSSRはSelective Hydration(選択的ハイドレーション)と密接に連携します。 ページ全体のJSが読み込まれるのを待たず、到着したClient Componentから順番にハイドレーションを開始できます。 さらに、ユーザーが操作しようとした要素のハイドレーションを優先する仕組みもあります。

たとえば、ページ下部のコメント欄がまだハイドレーション中でも、ユーザーがそこをクリックすると、 Reactはそのコンポーネントのハイドレーションを最優先で実行します。 これにより、Time to Interactive(TTI)の体感速度が大きく向上します。

sequenceDiagram
  participant S as サーバー
  participant B as ブラウザ

  S->>B: HTMLチャンク1(ナビ + フォールバック)
  Note over B: ナビを即座に表示
  Note over B: フォールバック(スケルトン)を表示
  S->>B: HTMLチャンク2(データ取得完了分)
  Note over B: スケルトンを実コンテンツに置換
  S->>B: JSバンドル(Client Component用)
  Note over B: Selective Hydration開始
  Note over B: ユーザー操作箇所を優先ハイドレーション
ストリーミングSSR + Selective Hydration: 段階的な配信とインタラクティブ化

理解度チェック

問題 0 / 40%
Q1

React Server Componentの最大の特徴はどれですか?

キーボード: 1〜4 で選択、Enter で回答