React 19 は「非同期と UI のつなぎ目」を言語機能のように整理しました。正しく使えば、ローディング分岐とロールバックの定型コードが大幅に減ります。
1. use(promise):レンダー中に非同期を読む
従来は useEffect で取得→useState に格納、という定型を書いていました。use は Promise をレンダー中に直接読み、解決まで Suspend します。ローディング状態の管理を <Suspense> に外出しできます。
"use client";
import { use } from "react";
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
// 解決まで Suspend → 親の <Suspense fallback> が表示される
const comments = use(commentsPromise);
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}
Server Component と組み合わせてストリーミングする
use の王道は「Server Component で fetch を開始(await しない)→ Promise を Client Component に渡す → クライアントで use する」パターンです。サーバーは待たずに HTML を流し始め、データが解決し次第ストリーミングされます。
// page.tsx(Server Component)
import { Suspense } from "react";
export default function Page() {
// ❗ await しない。Promise のまま子へ渡す
const commentsPromise = fetchComments();
return (
<Suspense fallback={<CommentsSkeleton />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}
これで「速い初期表示(LCP)」と「データ到着後の差し込み」を両立できます(Core Web Vitals 最適化参照)。
Context も条件付きで読める
use は早期 return の後でも呼べるため、useContext では書けなかった条件付き読み取りができます。
function Heading({ children }: { children?: React.ReactNode }) {
if (children == null) return null; // 早期 return
const theme = use(ThemeContext); // ✅ return の後でも OK(useContext は不可)
return <h1 className={theme.heading}>{children}</h1>;
}
2. use の致命的な落とし穴:レンダー内で Promise を作らない
これは事故が最も多いポイントです。Client Component の**レンダー内で Promise を生成すると、再レンダーのたびに新しい Promise ができ、永遠に解決しない(無限サスペンド)**状態になります。
// ❌ 絶対にダメ:レンダーのたびに新しい Promise → 無限サスペンド
function Bad() {
const data = use(fetch("/api/data").then((r) => r.json()));
return <div>{data.title}</div>;
}
正解は、Promise の生成元をレンダーの外に置くことです。
- Server Component で生成して props で渡す(前章のパターン。最推奨)。
- キャッシュ層(モジュールスコープや専用キャッシュ)から同一の Promise を返す。
- イベントハンドラやデータライブラリ(TanStack Query 等)に任せる。
データ取得の再フェッチ・キャッシュ・無効化まで必要なら、use を直接使うより TanStack Query のような専用ライブラリが適します。use は低レベルの基礎 API であり、キャッシュ戦略は持ちません。適材適所が KISS です。
3. useOptimistic:即時フィードバックと自動ロールバック
ネットワーク応答を待たずに UI を先に更新すると、体感速度が劇的に上がります。useOptimistic は、この「楽観的更新」をロールバックの手書きなしで実現します。
const [optimisticState, addOptimistic] = useOptimistic(actualState, updateFn);
actualState:保留中の操作が無いときの実体の値。updateFn(currentState, optimisticValue):楽観的な状態を計算する関数。addOptimistic(value):楽観的更新を発火(アクション/トランジション内で呼ぶ)。
重要な性質:アクションが完了すると、optimisticState は自動的に actualState へ収束します。 成功時は実体が更新され、失敗時は実体が変わっていないので楽観的エントリは自然に消えます。手動ロールバックが不要です。
Server Action と組み合わせたチャット送信の例
"use client";
import { useOptimistic } from "react";
type Message = { text: string; sending?: boolean };
function Thread({
messages,
sendMessage,
}: {
messages: Message[];
sendMessage: (text: string) => Promise<void>; // Server Action
}) {
const [optimistic, addOptimistic] = useOptimistic(
messages,
(state, newText: string): Message[] => [...state, { text: newText, sending: true }],
);
async function formAction(formData: FormData) {
const text = String(formData.get("message") ?? "");
if (!text) return;
addOptimistic(text); // ① 即座に「送信中」で表示
await sendMessage(text); // ② 完了後、実体が更新され optimistic は収束
}
return (
<form action={formAction}>
<ul aria-live="polite">
{optimistic.map((m, i) => (
<li key={i}>
{m.text}
{/* 送信中であることを支援技術にも伝える */}
{m.sending && <small role="status"> 送信中…</small>}
</li>
))}
</ul>
<label htmlFor="msg">メッセージ</label>
<input id="msg" name="message" />
</form>
);
}
addOptimistic はアクション(<form action> や useTransition 経由)の中で呼ぶ必要があります。Server Action の詳細はNext.js 16 Server Actions 実践ガイドを参照してください。
4. アクセシビリティ:楽観的更新を「聞こえる」ようにする
即時更新は視覚的には親切ですが、スクリーンリーダー利用者にも状態変化を伝える必要があります。
- 動的に増える領域は
aria-live="polite"で囲み、追加・更新を読み上げさせる。 - 「送信中」「失敗」は
role="status"/role="alert"で通知する。 - 送信中のコントロールは
aria-busy={true}にし、見た目(半透明)と意味を一致させる。
楽観的更新の「速さ」は、見える人だけのものにしないこと。これがアクセシブルな UX です(WCAG 2.2 実装ガイド)。
5. セキュリティ:楽観的状態を「真実」にしない
useOptimistic が見せるのはあくまでクライアントの予測です。
- 権限判断に楽観的状態を使わない。 「楽観的に成功表示したから処理済み」と扱うのは危険。サーバーが唯一の真実です。
- 失敗時のフィードバックを必ず用意する。 自動で楽観的エントリが消えるだけだと、ユーザーは「送れたのか?」と迷います。アクション側でエラーを捕捉し、再送導線やメッセージを出します。
- 冪等性を担保する。 二度押しや再送に備え、サーバー側で重複排除する(Server Actions の冪等性)。
6. useOptimistic と TanStack Query、どちらを使う?
| 観点 | useOptimistic | TanStack Query の onMutate |
|---|---|---|
| スコープ | アクション/フォーム単位 | アプリ全体のキャッシュ |
| ロールバック | 自動(実体へ収束) | 手動(スナップショット退避) |
| 反映範囲 | そのコンポーネント | 複数画面・複数コンポーネント |
| 再フェッチ/キャッシュ | 持たない | 持つ(無効化・再取得) |
| 向いている場面 | 単一フォームの即時反映 | 横断的なサーバー状態管理 |
単一フォームの送信中表示なら useOptimistic が最小コスト。複数画面に同じ更新を波及させたい・キャッシュ戦略が要るなら TanStack Query が適します(使い分けの詳細)。
7. その他の React 19 アップデート(早見表)
| API | 用途 | 関連記事 |
|---|---|---|
useActionState | フォーム送信の結果状態を型安全に扱う | Server Actions |
useFormStatus | 送信中(pending)をボタン側で取得 | 同上 |
useTransition | 重い更新を「緊急でない」と印付けし応答性を守る | Core Web Vitals |
useDeferredValue | 入力の即時反映と重い再計算の分離 | 同上 |
| ref as prop | forwardRef 不要で ref を直接 prop に | — |
これらは互いに補完し合います。use / useOptimistic を中心に、フォームは useActionState、応答性は useTransition という役割分担で組み立てます。
8. テスト:体感ではなく挙動で守る
- 楽観的更新の検証は E2E が得意。 送信直後に「送信中」が出て、完了後に確定表示へ変わる流れを Playwright で表明します(Playwright E2E 設計)。
- 失敗時のロールバックも、API をモックして 500 を返し、楽観的エントリが消えてエラーが出ることを確認します。
useのサスペンドは、<Suspense>のフォールバックが見え、その後コンテンツに置き換わることを E2E で検証します。
9. アンチパターン
- ❌ レンダー内で Promise を生成して
useに渡す。 無限サスペンドの最大要因。生成はレンダー外で。 - ❌
useEffect+useStateで取得していた処理を、キャッシュ無しで全部useに置換。 再フェッチ・重複排除が要るならデータライブラリを使う。 - ❌ 楽観的状態を権限・確定判断に使う。 サーバーが真実。UI 予測にとどめる。
- ❌ 失敗時のフィードバックを省く。 自動で消えるだけでは不親切。エラーと再送導線を出す。
- ❌
aria-live/role=statusを付けない。 速さが見える人だけのものになる。 - ❌
addOptimisticをアクション外で呼ぶ。 トランジション/アクションの文脈で使う。
10. FAQ(よくある質問)
Q. use は useEffect でのデータ取得を完全に置き換える?
A. 取得そのものは置き換えられますが、キャッシュ・再フェッチ・無効化は持ちません。それらが必要ならデータライブラリ(TanStack Query 等)を併用します。
Q. なぜ use はフックなのに条件分岐で呼べる?
A. use は他のフックと内部実装が異なり、条件付き・早期 return 後でも呼べるよう設計されています。ただし依然としてコンポーネント/フック内でのみ使えます。
Q. useOptimistic の失敗時、ロールバックは自分で書く?
A. 不要です。アクション完了時に optimisticState が actualState へ自動収束します。失敗時は実体が変わらないため楽観的エントリは消えます。エラー表示だけ用意します。
Q. use(promise) で無限ローディングになる。
A. ほぼ確実に「レンダー内で Promise を作っている」のが原因です。Server Component かキャッシュ層で生成し、同一の Promise を渡してください。
Q. これらは Next.js 専用?
A. いいえ、React 19 の機能です。ただし use のストリーミングは Server Components を持つフレームワーク(Next.js 等)と組み合わせると最大限に活きます。
まとめ:非同期と楽観性を「言語機能」として扱う
React 19 の use と useOptimistic は、これまで定型コードで書いていた「ローディング分岐」と「楽観的更新のロールバック」を、フレームワークの基礎機能へと引き上げました。
use(promise)でレンダー中に非同期を読み、<Suspense>に状態を委ねる。- Promise はレンダー外(Server Component / キャッシュ)で生成し、無限サスペンドを避ける。
useOptimisticで即時フィードバックを見せ、完了時に自動収束させる。- a11y(aria-live / role=status)とセキュリティ(サーバーが真実・冪等性) を必ず添える。
- 適材適所。 横断的なキャッシュは TanStack Query、単一フォームは
useOptimistic。
体感の速さは信頼に直結します。新 API を正しく使えば、少ないコードで、速く・安全で・誰にでも届く UX を作れます。
リアルタイム性や即応性が価値を生むアプリの設計・実装が必要な場合は、お気軽にご相談ください。 下記の事例では、複数人が同時に編集しても破綻しない、応答性重視のリアルタイムアプリを設計・実装した過程を紹介しています。