useOptimistic は、アクションが終わる前に、その結果を画面に表示できるようにするReact 19のフックです。アクションの中で楽観的な値をセットすると、Reactはアクションが保留中の間それを表示し、その後本当のstateに戻ります。リクエストが失敗していれば本当のstateは一度も変わっていないので、画面は自動的に元に戻ります。
この例の偽のAPIは本物のリクエストの代わりで、3回に1回失敗するので、両方の結果を確かめられます。
1秒ずつ間をおいて、ボタンを3回クリックしてください。最初の2回はすぐに切り替わり、保存済みの行が1秒後に追いつきます。3回目もすぐに切り替わりますが、リクエストが失敗すると元に戻ります。calls % 3 を calls % 2 に変えると、2回に1回失敗します。
構文
const [optimisticState, setOptimistic] = useOptimistic(state, updateFn?);
stateは本当の値で、通常はuseState、props、useActionStateから来ます。保留中のアクションがなければ、optimisticStateはまさにこの値です。setOptimistic(value)は、現在のアクションが実行されている間、楽観的な値をセットします。updateFnは省略可能です:(currentState, optimisticValue) => nextState。これがあると、setOptimisticは新しい値全体ではなく変化を受け取ります。後述のreducerの形を参照してください。
1回のクリックの流れは常に同じです。
- アクションの中で
setOptimisticを呼びます。画面はすぐに更新されます。 - アクションがリクエストをawaitします。
- 成功したら、本当のstateを更新します(
awaitの後ならstartTransitionの中で)。 - アクションが終わります。
optimisticStateは再びstateに従います。成功なら保存された値、失敗なら古い値です。
例にロールバックのコードがないのは、ステップ4のおかげです。catch ブロックはエラーメッセージをセットするだけです。
アクションの中で実行しなければならない
楽観的な値はトランジションが保留中の間しか存在しないので、setOptimistic はその中で呼ぶ必要があります。次のものはすべて該当します。
startTransition(またはuseTransitionのstartTransition)に渡す関数<form action={...}>や<button formAction={...}>に渡す関数- useActionStateに渡すアクション
普通の onClick から呼ぶと、楽観的な値には生きる場所となるアクションがありません。Reactはすぐにそれを元に戻し、開発中は「An optimistic state update occurred outside a transition or action」と記録します。
await の後に起きる本当のstateの更新は、上の startTransition(() => setLiked(next)) のように、それ自身の startTransition で包んでください。await の後では、Reactはまだ前のトランジションの中にいることがわからないからです。
reducerの形を使ったメッセージのリスト
楽観的なstateがリストのときは、第2引数に更新関数を渡します。するとセッターは新しい項目を受け取り、Reactはそれをその時点の本当のリストに追加します。
違うテキストのメッセージを、1つずつ3つ送ってください。それぞれ「(sending...)」付きで薄く表示され、届くとはっきり表示されます。3つ目はチャットから消え、「Not sent」として表示されます。代わりにすばやく送ると、最後のリクエストが終わるまで3つとも薄いままです。Reactは保留中のすべてのアクションが終わるまで楽観的な値を保つからです。フォームアクションはすでにトランジションなので、ここでは addOptimistic に独自の startTransition は必要ありません。
各楽観的な項目には、本物の項目と衝突しないkeyを付けてください。例では pending- とテキストを使っているので、同じテキストを2回送って両方が保留中だと衝突します。実際のアプリならクライアントでIDを作るでしょう。
複数の更新を同時に
更新関数は現在のstateを受け取るので、楽観的な更新は積み重なります。すばやくクリックすると、保留中の各クリックが前のものの上に適用されます。
すばやく4回クリックしてください。カートにはすぐに4と表示されます。リクエストが終わると、数は3に落ち着きます。3つ目のリクエストが失敗したからで、コンソールもそう伝えています。Reactは保留中のすべてのアクションが終わるまで楽観的な値を保ち、その後本当の数を表示します。
使うべきとき、使うべきでないとき
楽観的な更新は、ほぼ必ず成功し、簡単に取り消せる操作に向いています。いいね、スター、トグル、名前の変更、メッセージの追加などです。よくあるケースでは、ユーザーはスピナーを目にしません。
間違った「完了」が誤解を招く場面では避けてください。支払い、アカウントの削除、結果がわかる前にユーザーが行動するかもしれないものなどです。そうした場合は、useActionState やuseTransitionの isPending で保留中の状態を表示し、本当の答えを待ちます。
ロールバックが起きたときは、必ずユーザーに伝えてください。黙って元に戻る値はバグのように見えます。上の2つの例では、エラーメッセージを普通のstateに保ち、アクションより長く残るようにしています。
useActionStateと組み合わせる
useOptimistic と useActionState は相性がよいです。useActionState のアクションはすでにトランジションなので、その最初で楽観的な値をセットでき、返されるstateが、楽観的な値が戻る先の本当の値になります。
const [state, formAction] = useActionState(async (previous, formData) => {
const title = formData.get('title');
setOptimisticTitle(title);
const saved = await saveTitle(title);
return { title: saved.title };
}, { title: 'Untitled' });
const [optimisticTitle, setOptimisticTitle] = useOptimistic(state.title);
保存の実行中、ページには新しいタイトルが表示されます。解決すると、state.title が保存された値を持ちます。saveTitle が例外を投げると、アクションのエラーは最も近いエラーバウンダリに送られます。代わりにメッセージ付きで古いタイトルを表示したいなら、アクションの中でエラーを捕まえ、前回のstateとエラーを返してください。
よくある間違い
アクションの外でセッターを呼ぶ。楽観的な値は画面に残らず、開発中はReactが警告します。startTransition で包むか、フォームアクションの中に移してください。
本当のstateの更新を忘れる。成功したら、アクションは useOptimistic が映しているstate(setLiked、setMessages)を変えなければなりません。そうしないと、アクションが終わったときに楽観的な値が消え、成功したリクエストがロールバックのように見えます。
await後の本当のstateの更新をトランジションなしで行う。更新自体は反映されますが、Reactがそれをアクションの終わりとは違うタイミングで表示することがあります。3つの例すべてと同じように、startTransition で包んでください。
楽観的な値を信頼できる情報源として使う。リクエストを送ったり合計を計算したりするのは、本当のstateから行ってください。楽観的な値は表示専用で、いつ捨てられてもおかしくありません。
よくある質問
useOptimisticは何をするものですか?
非同期のアクションの実行中にすぐ変えられる、stateのコピーを与えます。アクションが終わると、そのコピーは再び本当のstateに従います。その時点の本当のstateは、保存された結果か、古い値のどちらかです。
useOptimisticはエラー時にどうやって元に戻すのですか?
特別なエラー処理は必要ありません。楽観的な値はアクションが保留中の間だけ存在します。リクエストが失敗して本当のstateを一度も更新しなければ、フックは再び本当のstate、つまりクリックする前の値を表示します。
"An optimistic state update occurred outside a transition or action" が出るのはなぜですか?
セッターが普通のイベントハンドラから呼ばれたからです。startTransition に渡す関数の中、<form action> の中、または useActionState のアクションの中で呼んでください。
useOptimisticの第2引数は何ですか?
省略可能な更新関数 (currentState, optimisticValue) => newState で、reducerのようなものです。これがあると、セッターは変化(新しいメッセージ、+1)だけを受け取り、複数の更新が同時に保留中でも、Reactが楽観的なstateを計算します。
useOptimisticはフォーム専用ですか?
いいえ。どのトランジションでも動くので、フォームアクションだけでなく、startTransition(async () => { ... }) を呼ぶボタンでも使えます。