useOptimistic は、非同期 Action の結果を待たずに一時的な UI を表示する Hook です。投稿、いいね、リスト追加のように、成功を見込んで即時の反応を返したいときに使います。
Action 中だけ仮の値を見せる
import { useOptimistic, useState, useTransition } from 'react';
function LikeButton() {
const [liked, setLiked] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);
function toggle() {
const nextLiked = !optimisticLiked;
setError(null);
startTransition(async () => {
setOptimisticLiked(nextLiked);
try {
const saved = await saveLike(nextLiked);
startTransition(() => setLiked(saved));
} catch {
setError('いいねを保存できませんでした');
}
});
}
return <>
<button onClick={toggle} disabled={isPending}>
{optimisticLiked ? 'いいね済み' : 'いいね'}
</button>
{error && <p role="alert">{error}</p>}
</>;
}
この例では setOptimisticLiked が即座にボタン表示を変え、saveLike が成功した値を setLiked へ反映します。await 後の state 更新をもう一度 startTransition で包むのは、現行 React ではその更新が自動的に transition 扱いにならないためです。保存に失敗した場合は基準 state を変えず、仮の表示が戻る理由をエラーメッセージで伝えます。処理中はボタンを無効にして、操作の重複を避けます。
setter を Action の中で呼ぶと、optimisticLiked はすぐ変わります。Action が終わると、渡した基準値 liked が最終表示を決めます。
流れは「基準値を表示する → Action 開始と同時に仮の値を表示する → サーバー結果で基準値を更新する → 仮の値が消える」です。仮の値を永続的な正解として扱わないことが重要です。サーバーが別の値を返せば、最終的にはその値が表示されます。
失敗と競合を設計する
失敗時は基準値が更新されないため、楽観的な表示は元へ戻ります。ユーザーへエラーを知らせる UI は別途必要です。リストや複数の関連値を更新する場合は reducer を渡し、Action 中に基準データが変わっても最新の値から計算できるようにします。
setOptimistic を transition や Action の外で呼ぶと警告され、表示もすぐ戻ります。通常の state の置き換えとして使わないでください。
複数回の操作を許す UI では、同じ Action が同時に走ったときの順序も考えます。二重送信を disabled で防ぐ、各操作に一意の ID を付ける、失敗した項目だけ再試行可能にする、といった UX が必要になることがあります。楽観的更新は通信エラーを消す機能ではありません。
reducer を渡す場面
リストへ仮のコメントを追加するように、現在値に action を適用する場合は第2引数に reducer を渡します。基準となるコメント一覧が待機中に更新されても、reducer は新しい一覧に対して仮のコメントを重ねられます。単純な setter だけで配列を閉じ込めるより、競合に強い設計になります。
まとめ
useOptimistic は待機時間を感じさせないための一時的な UI state です。成功後の正しい基準値、失敗時の復帰、エラー通知まで設計します。