フロントエンドの世界へようこそ。Reactを触り始めて数年、`useEffect`の依存配列地獄に頭を抱え、レンダリングの回数と戦い続けている君なら、きっと「UXの最大化」という壁にぶつかっているはずだ。
「サーバーからのレスポンスを待っている間の、あのコンマ数秒のラグをどうにかしたい」。そう思ったことはないか? 現代のWebアプリケーションにおいて、100msの遅延は「重い」と判断される。そこで登場するのが `useOptimistic` だ。今日は、React 18以降のパラダイムである「楽観的更新」の勘所を、現場の泥臭い知見を交えて解説する。
—
1. なぜ「楽観的」なのか?
ユーザーはせっかちだ。ボタンを押した瞬間、UIが即座に反応することを期待している。
これまでの泥臭い実装では、`useState`で状態を管理し、APIを叩く前に値を更新し、エラーが起きたら元の値に戻す…という処理を書いていたはずだ。だが、これだと競合状態(Race Condition)の制御が極めて面倒になる。「API通信中に他の操作が来たらどうする?」「再レンダリングのタイミングでUIがチラつかないか?」――これらを解決するのが `useOptimistic` というフックだ。
こいつの本質は、「現在の状態」と「サーバー反映待ちの仮の状態」を、Reactのレンダリングパイプラインの中で透過的に切り替えることにある。
2. useOptimistic の魔法:ブラウザの裏側
Reactのレンダリングプロセスにおいて、`useOptimistic` は「現在の状態」をベースに、アクションが開始された瞬間に「一時的な状態」を計算してマージする。
重要なのは、これが純粋な計算処理だということだ。副作用(API通信など)そのものは外側で管理し、`useOptimistic` は「今、UIはどう見えるべきか」という状態のスイッチングだけに専念する。これにより、ブラウザのメインスレッドを無駄にブロックせず、スムーズなDOM更新が可能になる。
3. 実践:洗練された楽観的更新のコード
では、現場でそのまま使える実装を見てみよう。ここでは「いいね!」ボタンを例にする。
import { useOptimistic, useTransition } from ‘react’;
// サーバー通信を模した関数(実際はfetchやServer Actionsなど)
async function updateLikeStatus(id: string, isLiked: boolean) {
// ここでAPIリクエストを投げる
await new Promise((resolve) => setTimeout(resolve, 1000));
return { id, isLiked };
}
export function LikeButton({ id, initialLiked }: { id: string; initialLiked: boolean }) {
const [isPending, startTransition] = useTransition();
// 第1引数: 現在の確定状態
// 第2引数: 楽観的更新を適用する関数
const [optimisticLiked, addOptimisticLiked] = useOptimistic(
initialLiked,
(currentLiked, newLiked: boolean) => newLiked
);
const toggleLike = async () => {
// 1. 即座にUIを更新(楽観的更新)
startTransition(() => {
addOptimisticLiked(!optimisticLiked);
});
try {
// 2. 実際のサーバー処理
await updateLikeStatus(id, !optimisticLiked);
} catch (e) {
// 3. エラー時は自動的に値が元の状態(initialLiked)に戻るのがReactの仕様
console.error(“通信失敗”, e);
}
};
return (
);
}
このコードの「賢い」ポイント
1. `useTransition` の重要性: `startTransition` を使うことで、この状態更新を「低優先度」としてマークできる。これにより、もし他に急ぎの入力処理などがあれば、そちらが先に処理され、ユーザーの操作感を阻害しない。
2. クリーンアップの自動化: 驚くべきことに、`useOptimistic` はアクションが終了(あるいは失敗)した時点で、自動的に「楽観的な状態」を破棄して「確定した状態」へと戻る。自分で `setState` で元に戻すようなボイラープレートコードは不要だ。
4. シニアからのアドバイス:副作用との付き合い方
ここで一つ、初心者が陥りやすい罠がある。
`useOptimistic` は「UIの状態」を楽観的に見せているだけで、サーバーの状態を書き換えているわけではない。もし、`useOptimistic` が変わった瞬間に `useEffect` で別の副作用を走らせようとすると、無限ループや不整合の温床になる。
- 鉄則: 副作用(ログ送信、Analytics、別コンポーネントへのデータ同期)は、`useOptimistic` の状態更新をトリガーにすべきではない。あくまで「ユーザーへのフィードバック」としてUI層に閉じ込めること。
- 整合性の限界: 楽観的更新は「成功する確率が高い操作」に限定すべきだ。例えば、残高が足りるか微妙な送金処理にこれを使うと、UIが戻る際の「カクつき」がユーザーに不信感を与える。
まとめ
`useOptimistic` を使いこなすことは、単に便利なAPIを使うことではない。「ユーザーが感じる遅延」と「システムが持つ物理的な遅延」の間のギャップを、数学的かつ美しく埋めることだ。
最初は難しく感じるかもしれないが、まずはシンプルな「いいね」ボタンや「リストの削除」といった操作から導入してみてほしい。フロントエンドの体験は、こうした細かい「配慮」の積み重ねで一流に変わる。
君のアプリケーションが、ユーザーにとって「吸い付くような操作感」を持つものになることを期待している。もし詰まったら、またいつでも聞きに来てくれ。

コメント