React 19: サーバーアクションとオプティミスティックUI(ゼロラグ体験)

300msのインタラクションの罠
ユーザーが「いいね」を押したり、商品をカートに追加したり、コメントを送信したりする際に、ネットワークの往復に200〜500ms待たされると、アプリケーションは動作が遅く、反応が鈍いと感じられます。しかし、自動ロールバック機能なしにローカルの状態を安易に変更すると、リクエスト失敗時に「ゴーストデータ」が生まれてしまいます。
React 19は、この問題をネイティブに解決します。 サーバーアクションとuseOptimistic、useActionStateフックを組み合わせることで、Reactは知覚レイテンシー0msの即時フィードバックを提供しつつ、バックエンドのミューテーションが失敗した場合には絶対的な状態整合性を保証します。
この詳細な解説では、React 19がサードパーティの状態管理ライブラリなしに、どのようにして並行トランジション、サーバーサイド実行、そして即時のオプティミスティック・ロールバックを協調させるのかを探ります。
React 19の状態更新を担う三つの要素
React 19は、煩雑になりがちなフォームの状態管理を置き換えるために、協調して動作する3つの特化したプリミティブを導入しました。
| フック / プリミティブ | 主な責務 | 実行場所 | 主要機能 |
|---|---|---|---|
| Server Actions | サーバーサイドでのデータミューテーションとDB書き込み | サーバー環境限定 | プログレッシブエンハンスメント、FormDataの自動シリアライズ |
| useActionState | フォームアクションの状態、エラーハンドリング、pendingステータス管理 | クライアントコンポーネント | [state, formAction, isPending] を返す |
| useOptimistic | ゼロレイテンシーの即時UIレンダリング | クライアントコンポーネント(Transition内) | アクションが失敗した場合、サーバーの正しい状態に自動復帰 |
オプティミスティックUIのライフサイクルとロールバックの流れ
低速な3G接続環境やサーバータイムアウトが発生した際に、ユーザーがフォームを送信すると裏側では何が起こるのでしょうか?
本番環境を想定した完全なアーキテクチャ: いいねとコメント
"use server";
import { revalidatePath } from "next/cache";
export type ActionResponse = {
success: boolean;
likesCount?: number;
error?: string;
};
export async function toggleLikeAction(
postId: string,
currentLikes: number
): Promise<ActionResponse> {
try {
// Simulate database write
await new Promise((resolve) => setTimeout(resolve, 800));
// Random failure simulation for resilient rollback demonstration
if (Math.random() < 0.1) {
throw new Error("Network timeout while updating likes table");
}
const updatedLikes = currentLikes + 1;
revalidatePath(`/blog/${postId}`);
return { success: true, likesCount: updatedLikes };
} catch (err: any) {
return { success: false, error: err.message || "Failed to register like" };
}
}
"use client";
import { useOptimistic, useTransition, useState } from "react";
import { toggleLikeAction } from "@/actions/postActions";
interface LikeButtonProps {
postId: string;
initialLikes: number;
}
export function LikeButton({ postId, initialLikes }: LikeButtonProps) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
// useOptimistic immediately reflects user intention
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment: number) => current + increment
);
const handleLike = async () => {
startTransition(async () => {
// 1. Instantly increment UI locally
setOptimisticLikes(1);
// 2. Perform Server Action
const result = await toggleLikeAction(postId, likes);
if (result.success && result.likesCount !== undefined) {
// 3. Confirm server state
setLikes(result.likesCount);
} else {
// 4. Auto-rollback occurs automatically because state was not modified!
alert(`Error: ${result.error || "Could not save like"}`);
}
});
};
return (
<button
onClick={handleLike}
disabled={isPending}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full bg-red-50 dark:bg-red-950/40 text-red-600 dark:text-red-400 font-semibold hover:scale-105 active:scale-95 transition-all shadow-sm disabled:opacity-75"
>
<span>❤️</span>
<span>{optimisticLikes} Likes</span>
{isPending && <span className="animate-pulse text-xs text-gray-400">Saving...</span>}
</button>
);
}
完璧なオプティミスティックUIを実現するための4ステップチェックリスト
1. 常にTransitionでラップする
useOptimisticの値が一時的な状態を保持するのは、Reactのトランジションがアクティブな間だけです。アクションの呼び出しは常にstartTransitionでラップするか、<form action={...}>に直接渡してください。
2. 安定した一時IDを生成する
リストに項目をオプティミスティックに追加する場合、一時的なUUID (crypto.randomUUID())を生成してください。これにより、サーバーが真のデータベースIDを生成する前に、Reactがリスト項目を正しくキー付けできます。
3. Promiseのrejectとサーバーエラーの両方を処理する
サーバーアクションは、未処理のPromiseリジェクトをスローするのではなく、構造化された{ success: false, error: "..." }オブジェクトを返すようにしてください。これにより、クライアントコンポーネントはロールバック時に分かりやすいトースト通知を表示できます。
4. 変更があった箇所のみを再検証する
Next.jsのサーバーアクションでは、粒度の細かいrevalidatePathやrevalidateTagを使用してください。これにより、ページ全体のキャッシュを無効化し、コストの高いフルページ再レンダリングを強制するのを避けることができます。
メンタルモデル・フラッシュカード
オプティミスティックUI更新
オプティミスティックUI更新
useOptimisticフック
useOptimisticフック
プログレッシブエンハンスメント
プログレッシブエンハンスメント
インタラクティブな知識チェック
React 19のuseOptimisticで、オプティミスティックな値が元の状態に戻る原因は何ですか?
よくある質問(FAQ)
いいえ。useOptimisticは非同期トランジション中のペンディング状態のミューテーションを扱うために特化して設計されています。フォーム送信におけるクライアントサイドのフェッチングライブラリを置き換えることはありますが、テーマの切り替えやオーディオプレイヤーのような、ページをまたぐクライアント専用の状態には、Zustandのようなグローバルストアが依然として理想的です。
はい! onClickハンドラやuseEffect、カスタムフック内でstartTransition(async () => { await myAction(); })のように呼び出しをラップすることで、サーバーアクションを起動できます。
React 19はuseActionStateとuseFormStatusを通じてisPending状態を公開します。これにより、サーバーアクションの実行中に送信ボタンを無効にしたり、入力をデバウンスしたりすることが可能になります。
おすすめの記事
ブラウザで動く無料の開発者ツール
AI CLIログの整形、Cron式の作成、JWTの解析、chmod権限の計算をオフラインで実行。
関連記事

TanStack Query v5とNext.js 15: Optimistic Updates、Cache Sync、Server Actions
TanStack Query v5とNext.js 15を組み合わせたOptimistic Updates、Cache Sync、Server Actionsの実装を、本番環境レベルのアーキテクチャとコード例で解説する包括的なガイドです。
続きを読む
React 19 Actions実践ガイド: useActionState, useOptimistic & Server Actionの回復性
React 19 Actionsの実践的な使用法を解説する包括的なガイド。useActionState, useOptimistic, Server Actionの回復性を本番環境レベルのアーキテクチャとコード例で紹介します。
続きを読む
Next.js15のキャッシュコンポーネントとServer Actions:完全な本番環境アーキテクチャガイド
Next.js15のキャッシュコンポーネントとServer Actionsを網羅した、本番環境レベルのアーキテクチャとコード例を含む完全ガイドです。
続きを読む