React 19 実践:Concurrent 機能・Actions・新 Hooks

前端工程

React 19 でアップグレードする価値

React 19 はこれまで実験的だった並行(Concurrent)機能を安定 API としてまとめ上げ、長年の弱点だった「フォーム / データ変更」を補完しました。核となる考え方は一文で言えます:書き込みは Action で包み、読み取りは並行プリミティブで包むことで、待っている間も UI が固まらないようにする

機能 React 18 React 19
Server Components 実験的(Next.js 等) ドキュメント級の安定機能に
フォーム送信 手書き useState + onSubmit useActionState / <form action>
Promise / Context の読み取り use は不可 use() でレンダー中に読み取り可能
ref useRef + forwardRef のみ Props として直接渡せる
楽観的更新 手書きの状態切り替え useOptimistic
並行トランジション useTransition useTransition + 強化された useDeferredValue

Actions:書き込みをファーストクラスに

Action とは「React が待機できる」関数です。<form action>useActionState と組み合わせると、React が pending 状態を自動管理し、送信ボタンを無効化します。

"use client";
import { useActionState } from "react";

async function subscribe(prevState: { ok: boolean; msg: string }, formData: FormData) {
  const email = formData.get("email");
  const res = await fetch("/api/subscribe", {
    method: "POST",
    body: JSON.stringify({ email }),
  });
  if (!res.ok) return { ok: false, msg: "登録に失敗しました" };
  return { ok: true, msg: "登録完了!" };
}

export function SubscribeForm() {
  const [state, formAction, pending] = useActionState(subscribe, {
    ok: false,
    msg: "",
  });

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <button disabled={pending}>{pending ? "送信中…" : "登録"}</button>
      <p>{state.msg}</p>
    </form>
  );
}

useFormStatus を使えば、子コンポーネントから親フォームの状態をプロップス伝播なしで取得できます:

import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "保存中…" : "保存"}</button>;
}

API が返すフォームの初期値などの設定データは、JSON フォーマッタ で整形してから initialState に渡すと、大きなオブジェクトを手書きするミスを防げます。


use:レンダー中に Promise と Context を読む

use() は React 19 で導入された「レンダー中の読み取り」プリミティブです。Promise を直接 await したり Context を読んだりでき、Suspense 境界でサスペンドします。

import { use } from "react";

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise); // resolve までサスペンド
  return (
    <ul>
      {comments.map((c) => (
        <li key={c.id}>{c.body}</li>
      ))}
    </ul>
  );
}

注意:use() はコンポーネントやフックのレンダー段階で呼ぶ必要があり、try/catch 外の条件分岐には書けません。これは普通の関数ではなく「ここは待つかもしれない」と React に伝えるシグナルです。


並行レンダリング:待っても UI を固めない

useTransition:重い更新を非緊急としてマーク

import { useTransition, useState } from "react";

function Search() {
  const [query, setQuery] = useState("");
  const [list, setList] = useState<Item[]>([]);
  const [isPending, startTransition] = useTransition();

  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    setQuery(value); // 入力は即座に反映
    startTransition(() => {
      setList(filterExpensive(value)); // 重い計算を transition 内へ
    });
  }

  return (
    <div>
      <input value={query} onChange={onChange} />
      <ul style={{ opacity: isPending ? 0.6 : 1 }}>
        {list.map((i) => (
          <li key={i.id}>{i.name}</li>
        ))}
      </ul>
    </div>
  );
}

useDeferredValue:値を自動的に「遅延」

入力とレンダーが分離している場合、useDeferredValue の方が手軽です:

const deferred = useDeferredValue(query);
const results = useMemo(() => filterExpensive(deferred), [deferred]);

違い:useTransition は「一連の状態更新」を包み、useDeferredValue は「遅延された値」を派生させます(通常 useMemo と併用)。


useOptimistic:楽観的更新が標準搭載

import { useOptimistic, useRef } from "react";

type Msg = { id: number; text: string; sending?: boolean };

function Chat() {
  const [messages, setMessages] = useOptimistic<Msg[]>([]);

  async function send(formData: FormData) {
    const text = formData.get("text") as string;
    setMessages((list) => [...list, { id: Date.now(), text, sending: true }]);
    await fetch("/api/chat", { method: "POST", body: JSON.stringify({ text }) });
  }

  return (
    <form action={send}>
      <input name="text" />
      {messages.map((m) => (
        <p key={m.id}>{m.text}{m.sending ? "(送信中)" : ""}</p>
      ))}
    </form>
  );
}

テーマカラーやボタンのコントラストは、カラー変換カラーコントラスト ツールで WCAG アクセシビリティを確認できます。


ref の Props 化とドキュメントメタデータ

ref はもはや特例ではない

function MyInput({ ref }: { ref: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} />;
}
// 利用:<MyInput ref={myRef} />  —— forwardRef は不要

ドキュメントメタデータをネイティブ対応

import { Title, Meta } from "react-dom";

function Page() {
  return (
    <>
      <Title>ページタイトル</Title>
      <Meta name="description" content="ページの説明" />
    </>
  );
}

React 18 からのスムーズな移行

  1. 依存を更新npm i react@19 react-dom@19@types/react を更新。
  2. forwardRef を置換ref を Props に直接書くだけ。旧書き方は非推奨だが当面互換。
  3. Actions を導入:「送信後に setState + loading」のフォームを useActionState へ移行。
  4. use で非同期を収束:Server Components 配下でデータ Promise を子へ渡し use() で読む。
  5. 並行を組み込む:リスト/検索系は useDeferredValue または useTransition で応答性を向上。

よくある質問 FAQ

Q1:Actions は Server Component でしか使えませんか?

いいえ。クライアント側でも useActionState で普通の非同期関数を包めます。Server Action は「関数がサーバー側で実行される」形式の一つに過ぎません。

Q2:use()await の違いは?

await は JS の実行時セマンティクス、use() は React のレンダーセマンティクスです。Suspense 境界でサスペンドし、並行スケジューリングによって中断・再レンダーされます。

Q3:useTransitionuseDeferredValue はどちらを?

「一連の更新全体を遅延」したいなら useTransition、「変化の速い値から遅い結果を派生」したいなら useDeferredValue + useMemo を。

Q4:React 19 は IE をサポートしますか?

いいえ、レガシーな挙動も完全に削除されています。モダンブラウザ環境が前提です。

Q5:楽観的更新が失敗したらどうロールバックしますか?

useOptimistic は Action が投げた例外時に自動で実状態へロールバックします。setMessages の「実データソース」を失敗時に変えないよう保証するだけで OK です。


おすすめツール

React 19 開発では、以下の ツール が役立ちます:


React 19 の本質は「Hooks が増えた」ことではなく、書き込み(Actions)と読み取り(並行/use)を統一されたメンタルモデルで管理することです。データ待ちでも UI は固まらず、変更送信時も状態を自管理する。Actions と並行プリミティブを握れば、现代 React の鍵を手にしたことになります。

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

#React#React 19#并发渲染#前端#useTransition#Server Components#Actions