目次
28 章

React 19 新機能まとめ

Actions・useActionState・useFormStatus・useOptimistic・use()・React Compiler・ref as prop。第22~27章に散らばって登場した React 19 新機能を一か所にまとめて整理します。

第27章までで第4部の本内容が締めくくられました。第4部では Server Components と App Router という大きな絵を描き、その上にデータフェッチ、Suspense、Server Actions を順番に重ねていきました。その過程で React 19 で新たに安定化された機能たちがあちこちに登場しましたが、本章で一か所に集めてカタログとして整理します。

この章は 2 種類の読者を同時に対象にしています。

  • 第4部を順を追ってついてきた読者 には、散らばった断片を 1 つの絵にまとめてくれる復習章。
  • React 18 までは馴染みがあるが、19 からはあまり知らない読者 には、本書を最初から読む時間がないときに 18 → 19 の変化を一気に追いつく入口。

後者のために、本章は単独で読んでも流れがつながるように書きます。各節は「どんな問題を解こうとしたのか」 → 「API の形」 → 「いつ使わなくてよいのか」の順で進めます。

第4部の足跡 — React 19 機能が登場した地点 #

登場した React 19 機能
第25章 データフェッチとキャッシュ(間接)Server Component の async 関数モデル
第26章 Suspense と use()use() フック、Promise を prop として渡すパターン
第27章 Server Actions とフォームActions API、useActionStateuseFormStatususeOptimistic
全領域ref as prop、React Compiler、Document Metadata

上のカタログを節ごとにもう一度押さえていきます。

1. Actions API — フォームと mutation の新標準 #

何を解こうとしたのか #

React 18 までフォーム mutation は第9章で作ったパターンが標準でした。

React 18 時代のフォーム mutation
'use client';

import { useState, type FormEvent } from 'react';

function Form() {
  const [text, setText] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setSubmitting(true);
    try {
      const res = await fetch('/api/items', { method: 'POST', body: JSON.stringify({ text }) });
      if (!res.ok) throw new Error('失敗');
      setText('');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'エラー');
    } finally {
      setSubmitting(false);
    }
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

毎回同じボイラープレートが繰り返されていました。submitting state、error state、結果 state、そしてこれらを束ねる try/catch/finally。

React 19 の Actions API はこのパターンを標準化します。「非同期関数をフォームに直接つなげば、pending と結果を React がよろしく追跡する」というモデルです。

API の形 #

Action は 引数を受け取って Promise を返す非同期関数 です。

Action 関数のシグネチャ
async function action(prevState: State, payload: FormData): Promise<State> {
  // 検証、mutation、結果の返却
}

ここでの核心は 2 つ。

  1. 第 1 引数が 以前の stateuseActionState がこのスロットに直前の呼び出しの戻り値を自動で入れてくれます。
  2. 戻り値が次の呼び出しの prevState になります。state を自分で管理するわけです。

Next.js の 'use server' ディレクティブが付くと Server Action になり、クライアントから呼び出すと自動でサーバーで実行されます(第27章で扱った RPC メカニズム)。

<form action={fn}> — ブラウザネイティブとの出会い #

React 19 は <form>action prop が関数も受け取るように拡張しました。

action prop に関数を直接つなぐ
<form action={myAction}>
  <input name="text" />
  <button>送信</button>
</form>

ブラウザの標準のフォーム送信が起こると、React がそれを横取りして myAction(formData) を呼び出します。JavaScript が無効化されていてもフォームは普通の POST リクエストとして動作します(Server Action の場合)。第27章で見た progressive enhancement の基盤がまさにこのモデルです。

いつ使わなくてよいのか #

フォームではなく単純な mutation(例: いいねボタンのクリック)なら、Action API を無理にねじ込む必要はありません。onClick ハンドラの中で fetch や Server Action を直接呼び出せばよいです。Actions の価値は フォームデータ + pending + 結果 state が一緒についてくるときに光ります。

2. useActionState — フォームの state を 1 つのフックに束ねる #

何を解こうとしたのか #

上の Actions API だけでもフォーム送信自体はできますが、結果メッセージを画面に表示する には別の state が必要です。

結果表示 — 散らかったバージョン
const [result, setResult] = useState<{ message: string } | null>(null);

async function action(formData: FormData) {
  const res = await myAction(formData);
  setResult(res);
}

useActionState は上のパターンを 1 フックに束ねます。

API の形 #

useActionState の使用
const [state, formAction, isPending] = useActionState(
  async (prevState, formData) => {
    // ... 検証および mutation
    return { message: '完了', success: true };
  },
  { message: '', success: false },  // 初期 state
);

return (
  <form action={formAction}>
    {state.message && <p>{state.message}</p>}
    <input name="text" />
    <button disabled={isPending}>送信</button>
  </form>
);

戻り値 3 つ。

  • state: Action の最後の戻り値(または初期 state)
  • formAction: <form action={...}> にそのまま渡せるラップされた関数
  • isPending: 現在送信中かを示す boolean

検証エラーを画面に表示 #

サーバーで検証した結果をそのまま画面に流せます。

src/app/actions.ts
'use server';

type State = { error?: string; success?: boolean };

export async function createPost(prevState: State, formData: FormData): Promise<State> {
  const title = (formData.get('title') ?? '').toString().trim();
  if (!title) return { error: 'タイトルを入力してください' };
  if (title.length > 100) return { error: 'タイトルは 100 文字以内で' };

  await db.posts.insert({ title });
  return { success: true };
}

検証をサーバーで行うのでクライアントから迂回できず、結果はフックが自動で画面に反映します。

いつ使わなくてよいのか #

フォーム送信の結果を画面に表示する必要がない場合(例: 単純な GET フォーム)、または useFormStatus だけで十分な子コンポーネント単位の表示なら、useActionState までは必要ありません。

3. useFormStatus — 子コンポーネントで pending を受け取る #

何を解こうとしたのか #

useActionStateisPending は同じコンポーネントでしか使えません。ですが SubmitButton のような 再利用可能なコンポーネント が親フォームの pending を知らないといけないなら、prop でいちいち下ろさないといけませんでした。

useFormStatus最も近い親 <form> の状態 を自動で購読します。

API の形 #

src/app/SubmitButton.tsx
'use client';

import { useFormStatus } from 'react-dom';

export default function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending, data, method, action } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? '送信中...' : children}
    </button>
  );
}

戻り値のうち最もよく使われるのは pending。残り(datamethodaction)はデバッグや進行率表示のような特殊ケースで使います。

注意 — react-dom から import #

useFormStatusreact ではなく react-dom から来ます。これはフックが DOM の <form> 要素に紐づいているからです。

いつ使わなくてよいのか #

フォームの中に SubmitButton が 1 つで同じコンポーネントで定義されているなら、useActionStateisPending だけで十分です。別のフックが必要になるのは 子コンポーネントが親フォームの状態を知らないといけない ときです。

4. useOptimistic — 楽観的更新 #

何を解こうとしたのか #

mutation の応答が届くまで UI が「以前の状態」のままに留まると、体感速度が落ちます。Twitter のいいねボタンがクリック即座に赤くなるような UX を作るには 応答を待たずに先に画面を更新する 必要があります。

伝統的にはこれを手で state を別に管理し、失敗時のロールバックロジックも自分で書かないといけませんでした。useOptimistic がこのパターンを標準化します。

API の形 #

useOptimistic の使用
'use client';

import { useOptimistic } from 'react';

type Item = { id: string; text: string };

function ItemList({ items }: { items: Item[] }) {
  const [optimisticItems, addOptimistic] = useOptimistic<Item[], Item>(
    items,
    (state, newItem) => [...state, newItem],
  );

  async function handleAdd(formData: FormData) {
    const text = (formData.get('text') ?? '').toString();
    const tempItem: Item = { id: 'temp-' + crypto.randomUUID(), text };
    addOptimistic(tempItem);
    await addItemAction(formData);
  }

  return (
    <>
      <ul>
        {optimisticItems.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
      <form action={handleAdd}>...</form>
    </>
  );
}

useOptimistic(state, reducer) のシグネチャ。reducer は (現在の state, 入力) => 新しい state。クリック即座に reducer が呼ばれて画面が更新され、サーバー応答で本物の state が更新されると React が 2 つのトラックを自動で同期します。

失敗時の自動ロールバック #

楽観的に描いていた UI は mutation が終わると実際の props に置き換えられます。失敗して props が更新されなければ自動で以前の状態に戻ります。つまり ロールバックロジックを自分で書く必要がありません

いつ使わなくてよいのか #

  • 応答が速い mutation(50ms 未満): わざわざ楽観更新しなくてもユーザーは感じません。
  • 結果が入力と異なって見えうる mutation(例: サーバーで ID やタイムスタンプを付与): 楽観表示と実際の表示が食い違って違和感を与えます。
  • 一貫性が重要なデータ(例: 決済、残高): 楽観的に見せて失敗時ロールバックされる方が大きな混乱です。

5. use() — Promise/Context を解く新フック #

何を解こうとしたのか #

Server Component で await が可能というモデルは強力ですが、Client Component でも Promise を解きたいとき があります。第26章で見た「Server で作った Promise を Client に渡す」パターンがその例です。

既存のフックルールは「条件文や繰り返し文の中でフックを呼んではならない」でした。useContext も同様でした。use() はこの 2 つを両方解いた新フックです。

API の形 — Promise #

src/app/ItemList.tsx (Client)
'use client';

import { use } from 'react';

type Item = { id: string; text: string };

export default function ItemList({ itemsPromise }: { itemsPromise: Promise<Item[]> }) {
  const items = use(itemsPromise);
  return <ul>{items.map(i => <li key={i.id}>{i.text}</li>)}</ul>;
}

use(promise) は Promise が届くまで Suspense fallback を見せ、届くとその値を返します。呼び出しコンポーネントの上位に <Suspense> がなければなりません。

API の形 — Context #

条件付き Context 使用
'use client';

import { use } from 'react';
import { ThemeContext } from './ThemeContext';

function Card({ showTheme }: { showTheme: boolean }) {
  if (showTheme) {
    const theme = use(ThemeContext);
    return <div className={theme}>...</div>;
  }
  return <div>...</div>;
}

useContext だったらコンポーネントの最上位でしか呼べませんでしたが、use条件文の中でも呼び出し可能 です。

いつ使わなくてよいのか #

  • Server Component ではただ await の方が単純です。use(promise) は主に Client Component が Promise を prop として受け取ったとき に意味があります。
  • Context を常に読むなら既存の useContext で十分です。use の価値は条件付き呼び出しが必要なときです。

6. React Compiler — 自動 memoization #

何を解こうとしたのか #

第14章で見た memo / useMemo / useCallback は強力ですが、いつどこに置くべきかを決めるコスト が大きかったです。そして依存配列を間違って書くと stale closure バグが起きました。

React Compiler(React 19 時点で RC 段階)はビルド時点にコードを分析して自動で memoization を適用します。手で useMemo / useCallback を書く負担が大きく減ります。

導入 #

React Compiler のインストール
pnpm add -D babel-plugin-react-compiler

Next.js 15 では next.config.ts でオプションをオンにします。

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

export default nextConfig;

ESLint プラグイン(eslint-plugin-react-hooks の React Compiler ルール)も一緒にオンにすると、Compiler が分析に失敗した箇所を事前に教えてくれます。

Compiler があっても手でやるべき境界 #

自動 memoization は万能ではありません。次の場合は依然として手でやらないといけません。

  • 参照同一性を外部に露出するコード — ライブラリにコールバックを渡すときに、そのライブラリが同じコールバックを仮定するなら手で useCallback
  • 明示的な依存追跡が必要なコードuseEffect の依存配列はそのまま手で。
  • Compiler が分析を諦める場合 — 動的な indexing や非常に動的なコード。ESLint プラグインが警告で教えてくれます。

いつ使わなくてよいのか #

小さなプロジェクトやパフォーマンスがボトルネックでない場合は Compiler をオンにしなくても構いません。ただし第14章の基本原理(「再レンダリングがコストを生む」)自体はそのまま有効なので、Compiler の有無に関わらず身につけておくとよいです。

7. ref as prop — forwardRef の終焉 #

何を解こうとしたのか #

React 18 まで子コンポーネントに ref を渡すには forwardRef で包まないといけませんでした。

React 18 までの forwardRef
import { forwardRef } from 'react';

const FancyButton = forwardRef<HTMLButtonElement, { label: string }>(
  function FancyButton(props, ref) {
    return <button ref={ref}>{props.label}</button>;
  },
);

型を 2 回書かないといけなくて、関数シグネチャも普通の関数と違って学習負担がありました。

React 19 からは ref を普通の prop のように 受け取れます。

API の形 #

React 19 — ref as prop
type Props = {
  label: string;
  ref?: React.Ref<HTMLButtonElement>;
};

function FancyButton({ label, ref }: Props) {
  return <button ref={ref}>{label}</button>;
}

forwardRef が消えて、ref はただ他の prop と同じように扱います。第20章(Context とジェネリックコンポーネント)で作ったジェネリックコンポーネントに ref を付ける場合もずっと単純になります。

マイグレーション #

既存の forwardRef コードはしばらくそのまま動作します(deprecated 警告が出て、後続のメジャーで除去予定)。新しいコードは ref as prop で、既存コードは時間があるときにマイグレーション — くらいが安全な流れです。

8. その他の変更 — 小さいけれど知っておくべきこと #

Document Metadata #

<title><meta><link> をコンポーネントの中のどこでレンダリングしても、React が自動で <head> にホイスティングします。

コンポーネントの中で <title>
function PostPage({ post }: { post: Post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.summary} />
      <h1>{post.title}</h1>
      <div>{post.body}</div>
    </article>
  );
}

Next.js では普通 export const metadatagenerateMetadata を使いますが、ライブラリコンポーネントが自分の metadata を自前で持っている場合に便利です。

Hydration エラーメッセージ #

React 18 では hydration mismatch が起きるとメッセージが曖昧でした。React 19 からは どのノードの何が食い違ったか を具体的に教えてくれます。デバッグ時間が大きく減ります。

Asset Loading — <link rel="preload" /> #

リソースをコンポーネントの中で直接 preload できる API が整理されました。

画像の preload
import { preload } from 'react-dom';

function HeroSection() {
  preload('/hero.jpg', { as: 'image' });
  return <img src="/hero.jpg" alt="ヒーロー" />;
}

<link rel="preload"> を自動で作って重複呼び出しを dedup します。第31章(パフォーマンスと Web Vitals)で LCP を引き下げる道具の 1 つとしてもう一度出会うことになります。

18 → 19 変化サマリーカード #

初めて 18→19 にジャンプする読者のための 1 ページカード。

領域React 18React 19
フォーム mutationuseState + fetch + try/catch<form action={fn}> + Actions
フォーム結果 state直接 useStateuseActionState
pending 表示直接 useStateuseActionStateisPending または useFormStatus
楽観的更新直接 state 管理 + ロールバックuseOptimistic
Promise を解くServer Component で await のみ+ use(promise)(Client)
条件付き Context不可(フックルール)use(Context) 条件付き OK
memoization直接 memo / useMemo / useCallback+ React Compiler(自動)
ref の伝達forwardRefref as prop
Document head外部ライブラリまたは metadata APIコンポーネントの中の <title> / <meta> 自動ホイスティング
Asset preload<link> 手動react-dompreload

試してみる — 18 → 19 ミニマイグレーション #

小さなフォーム 1 つを React 18 スタイルから 19 スタイルに移してみます。第4部第27章のゲストブックフォームを逆に 18 スタイルで書いた後、節を 1 つずつ適用しながら 19 に変えてみてください。

  1. 出発点 — React 18 スタイル: 第27章で作ったゲストブックの MessageFormuseState で text/submitting/error を別々に管理し、<form onSubmit={...}> に fetch を直接掛けるコードで書き直してください。(意図的に React 18 時代のコードを再現)
  2. 第 1 段階 — Actions の導入: 'use server' の付いた postMessage 関数を作り、<form action={postMessage}> でつないでください。submitting state がどう消えるかを観察します。
  3. 第 2 段階 — useActionState: 結果メッセージのための useState を useActionState に置き換えてください。検証エラーをサーバーで投げて、その結果が自動で画面に表示される流れを確認します。
  4. 第 3 段階 — useFormStatus: 送信ボタンを別の SubmitButton コンポーネントに分離し、useFormStatus で親フォームの pending を受け取るようにしてください。ボタンコードを一度作っておけばすべてのフォームで再利用される点を確認します。

3 段階を経た後で、React 18 時代のフォームコードと React 19 時代のフォームコードが同じ仕事をどれほど違うふうに表現するかが手に深く刻み込まれます。

練習問題 #

  1. 基本原理を文章でまとめる。Actions API の「関数が state を持つ」モデルと React 18 時代の「コンポーネントが state を持つ」モデルがどう違うかを 5 文以内にまとめてみてください。useActionState の第 2 戻り値(formAction)が React が関数に state を付与するメカニズムであるという点を押さえれば答えに近づきます。
  2. useOptimistic を使わない場合。次の 3 つの mutation のうち useOptimistic を適用するとかえって UX が悪くなり得るのはどれで、なぜそうなのかを説明してみてください。(a) Todo 項目の追加、(b) 決済処理、(c) コメントのいいね。それぞれについて mutation が失敗するときにユーザーが受ける印象がどう違うかを描写してみてください。
  3. React Compiler 影響評価。第14章の useMemo / useCallback 例コードを React Compiler がオンになっていると仮定して読み直してみてください。どのコードはそのままにしておいても自動で最適化され、どのコードは依然として手で意図を表現しないといけないかを分類してみてください。手で置くコードの共通点が「参照同一性を外部に露出する」点に到達すればよいです。

一行まとめ: React 19 はフォーム / mutation のすべてのボイラープレートを Actions API(useActionStateuseFormStatususeOptimistic)1 セットに整理し、Promise / Context をより柔軟に扱う use() フックを追加した。React Compiler が自動 memoization で第14章の手作業を減らし、forwardRef は ref as prop で単純になった。第4部第22~27章が上の道具たちの上で動作し、続く第5部と第6部も同じ土台の上に積み上がる。

次の章 #

本章で第4部(モダン Next.js)が完全に締めくくられます。次の 第29章 コンポーネントテスト — Vitest + Testing Libraryから第5部(運用・テスト・デプロイ)が始まります。第5部は「React を作れる」から「React で仕事をする」へ渡る橋です。最初の章である第29章では、我々が第4部までに作ったコンポーネントたちにどう安全網をかぶせるか、Vitest と React Testing Library の基本原理から始めて mocking とフックテストまで整理します。

X