React 19 と Server Components:実践的ディープガイド

前端工程

我々のチームが最初に踩した RSC の穴

去年、古い CRA アプリを Next.js App Router に移行した。自信満々でページ全体を非同期コンポーネントにし、その中で直接 await fetch を書いた。ローカルでは動いた——しかしステージングに上げると白紙になった。30 分調べて判明した原因は、onClick を使ったボタンとその親をサーバー側に置いたままにしていたことだ。サーバーコンポーネントには「イベント」という概念そのものが存在しない。

そのインシデントでひとつ分かった。RSC は「書き方を変える」ものではない。「どのコードがどのマシンで動くか」をもう一度考え直すものだ。これまでに踩した穴と、検証済みのやり方をここにまとめる。


1. React 19 で何が変わったのか

まずは純クライアント側の変更から。これらは RSC とは無関係だが、移行時には必ず出くわす。

use():ついに Promise も Context も読める

use() はレンダー中に Promise を直接 await でき、Context も読める。そして条件文の中でも呼べる(これが useContext との最大の違いだ)。

import { use } from "react";

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise); // useEffect なしで結果を直接取得
  return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}

Actions:「送信 → 待機 → 更新」がファーストクラスに

以前は isPendinguseState、フォームのリセットを自前で管理した。今は useActionState + サーバー Action で一気に片付く:

"use server";
async function subscribe(prevState: { ok: boolean }, formData: FormData) {
  const email = formData.get("email");
  // サーバーで動くので DB/シークレットに触れる
  await db.users.subscribe(String(email));
  return { ok: true };
}
"use client";
import { useActionState } from "react";

function Form() {
  const [state, formAction, pending] = useActionState(subscribe, { ok: false });
  return (
    <form action={formAction}>
      <input name="email" />
      <button disabled={pending}>{pending ? "送信中…" : "購読"}</button>
      {state.ok && <p>購読しました</p>}
    </form>
  );
}

useOptimistic:楽観的更新を手書きしない

const [optimistic, addOptimistic] = useOptimistic(
  messages,
  (cur, next: string) => [...cur, { text: next, sending: true }]
);

小さいけれど嬉しい 2 つの変更

  • ref を普通の prop として渡せる。forwardRef は不要に(旧 API も動く)。
  • ネイティブの <form><meta><title><link> をコンポーネント内に直接書ける。React が自動で <head> に持ち上げる。

2. RSC とは何か:実行モデル

Server Component はビルド時またはサーバーでレンダーされる。そのコードはブラウザのバンドルに入らない。返すのは DOM ではなく、特殊なシリアライズ形式(RSC Payload)だ。それをクライアントの React が組み立てる。

直感に反する点:

  • サーバーコンポーネントは巨大なサーバー専用ライブラリ(DB ドライバなど)を import できる。そのコードはそもそも配信されないからだ。
  • サーバーコンポーネントには useStateuseEffect、ブラウザイベントはない。リクエストごとに一度だけ走る。
  • クライアントコンポーネント("use client" 付き)は JS にバンドルされ、サーバー側でも初期 HTML として事前レンダーされる。

メンタルモデル:

リクエスト ──▶ サーバーコンポーネントツリー(Node で実行)
                 │ "use client" 境界にぶつかる
                 ▼
            RSC Payload をシリアライズ ──▶ ブラウザ
                                          │ クライアントコンポーネントが hydrate
                                          ▼
                                    インタラクティブなページ

3. 'use client' 境界の引き方

移行で最も頭を使う箇所だ。原則はひとつ:クライアント境界は葉ノードにできるだけ近づける

// サーバーコンポーネント(既定)
import { db } from "@/server/db";
import LikeButton from "./LikeButton"; // これはクライアントコンポーネント

export default async function Post({ id }: { id: string }) {
  const post = await db.posts.find(id); // 直接 DB を叩き、安全
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <LikeButton initial={post.likes} /> {/* このボタンだけが対話を必要とする */}
    </article>
  );
}
"use client";
export default function LikeButton({ initial }: { initial: number }) {
  const [likes, setLikes] = useState(initial);
  return <button onClick={() => setLikes(v => v + 1)}>👍 {likes}</button>;
}

よくある失敗:ページ全体を "use client" にする。そうすると純 CSR に逆戻りし、RSC の利益(バンドル削減、データソース直結)が全部消える。本当に対話やブラウザ API、useState が必要な最小のコンポーネントだけに "use client" を付ける。


4. サーバーコンポーネント内で直接データを取る

もう getServerSideProps のような儀式は不要だ。コンポーネント自体が async になる:

export default async function Dashboard() {
  const [orders, users] = await Promise.all([
    fetch("https://api/internal/orders").then(r => r.json()),
    fetch("https://api/internal/users").then(r => r.json()),
  ]);
  return <Charts orders={orders} users={users} />;
}

注意:クライアントコンポーネントに渡す props はシリアライズ可能でなければならない。関数、Map、クラスインスタンスは不可。振る舞いを渡したい?その関数はクライアントコンポーネントの内側で定義する。


5. Suspense によるストリーミング

重いクエリが遅くても、ページ全体を待たせる必要はない。Suspense で遅い部分を「くり抜く」:

import { Suspense } from "react";

export default function Page() {
  return (
    <main>
      <h1>レポート</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowReport /> {/* このコンポーネント内で重いクエリを await */}
      </Suspense>
    </main>
  );
}

サーバーはまず <h1> とスケルトンを送出し、SlowReport のデータができたらその「穴」を埋める(ストリーミング)。体感される初回描画はずっと速くなる。


6. キャッシュ:fetch は既定でキャッシュされる

RSC 環境では fetch は既定でキャッシュされる(cache: 'force-cache' 相当)。読みが多い場面では好都合だが、切り方を知っておく:

fetch(url, { cache: "no-store" });            // 常に最新
fetch(url, { next: { revalidate: 60 } });     // 60 秒以内はキャッシュを使う

fetch 以外の計算をリクエスト間で共有したいなら React.cache() を使う:

import { cache } from "react";

export const getUser = cache(async (id: string) => {
  return db.users.find(id); // 同一レンダー内で何度呼んでも DB は 1 回
});

7. 実際のプロジェクト移行で踩した穴

  1. Context はクライアントコンポーネントからしか下に流せない。 サーバーコンポーネントで Context Provider を定義するとエラーになる——Provider はクライアント境界の内側に置く。
  2. イベントハンドラはサーバーコンポーネントから下に渡せない。 onClick が必要?その層はクライアントコンポーネントにする。
  3. window/document/localStorage はサーバー側では undefined。 ブラウザ API へのアクセスは "use client" コンポーネント、useEffect、またはイベントハンドラの中に置く。
  4. サードパーティ製ライブラリが必ずしも互換とは限らない。 "exports were not found" やハイドレーションエラーが出たら、"use client" 対応版がないか確認するか、自分のクライアントコンポーネントで包んでから import する。
  5. サーバーコンポーネントに useState を書くな。 コンパイルすら通らず、境界を間違えている合図になる。

8. RSC を使うべきでないとき

RSC は銀の弾丸ではない。以下の場面では純クライアントのほうが向いている:

  • 対話性が強くデータが軽いツール系ページ(リッチテキストエディタ、お絵かき、IDE 風)。
  • ほぼ静的でバックエンド依存のないマーケティングページ(SSG/静的エクスポートのほうが軽い)。
  • チームが SSR のメンタルモデルに馴染む前に期限に追われているとき——無理に RSC を使うと、遅くて保守困難な「キメラ」になりがち。

よくある質問

Q1:RSC と SSR は同じ?

違う。SSR はクライアントコンポーネントをサーバーで HTML にレンダーする(コードはバンドルに残る)。RSC はコンポーネントがブラウザに届かず、サーバーで動く。両者はよく併用される。RSC がデータ/構造を、SSR が初回 HTML を担う。

Q2:クライアントコンポーネントの props に何故関数を渡せない?

props は RSC Payload のシリアライズを経るが、関数はシリアライズできないからだ。振る舞いが必要?クライアントコンポーネントの内側で定義するか、Actions 経由でサーバー側ロジックを呼ぶ。

Q3:「use client」をファイル先頭に書くと、そのファイルの全コンポーネントがクライアントになる?

そうなる。ファイルには境界はひとつだけで、"use client" はモジュール全体をクライアントモジュールにする。だからクライアントコンポーネントは小さな別ファイルにし、サーバーコンポーネントから import する。

Q4:React 19 は古いプロジェクトにそのまま使える?

クライアント機能(useuseOptimistic 等)は React のバージョンアップだけで良い。しかし RSC を得るには対応フレームワーク(Next.js App Router や RSC 対応のメタフレームワーク)が必要だ。生 webpack プロジェクトでは RSC は使えない。

Q5:ハイドレーション不一致をどう切り分ける?

典型的な症状は、サーバー側でレンダーした文字列とクライアントの初回レンダーが一致しないこと(タイムスタンプ、乱数、toLocaleString 等)。こうした値は useEffect 内で再代入するか、クライアント依存情報にはスケルトン占位を使う。


おすすめツール

RSC やコンポーネントと格闘するとき、ToolsKu の以下のツールが役立つ:


RSC の本質は、「コードはどこで動くか」という SPA 時代に隠された問いを、再びあなたの目の前に置くことだ。境界を正しく描けばバンドル削減の救世主になり、描き間違えば新たな混沌の源泉になる。

ブラウザローカルツールを無料で試す →

#React 19#Server Components#SSR#前端工程#Next.js