ローディングスピナーが与える心理的負担
私たちは皆、あの微かな苛立ちを感じたことがあります。「いいね」ボタンを押したり、チャットメッセージを送信したりした瞬間に、一瞬何も起きないあの感覚です。その後、グレーのスピナーが回り始め、サーバーとの往復500ミリ秒を経て、ようやくUIが更新されます。このわずかな遅延が、最新のアプリであっても動作が重く、鈍いものに感じさせてしまうのです。
これは、従来のWebパターンが変更を表示する前にサーバーの確認を待つために起こります。不安定な4G接続では、その待ち時間は簡単に1秒を超えてしまいます. 研究によると、ユーザーはわずか100ミリ秒の遅延でも感知するとされています。インターフェースが即座に反応しなければ、ユーザーはアプリが壊れているかのように感じてしまうのです。
React 19以前は、サーバーの処理が完了する前に成功状態を表示する「オプティミスティックUI(楽観的UI)」の構築は非常に面倒な作業でした。以前の状態を手動で追跡し、複雑なエラーハンドリングを記述し、APIが失敗した場合にはロールバックをトリガーする必要がありました。それはスパゲッティコードと同期バグの温床でした。
クイックスタート:初めてのオプティミスティック更新
新しいuseOptimisticフックは、そうした定型コード(ボイラープレート)を排除します。これにより、非同期アクションが実行されている間だけ存在する一時的な状態を定義できるようになります。
シンプルなメッセージボックスを例に考えてみましょう。データベースへの書き込みを待つ代わりに、メッセージを即座に画面に表示させることができます。
import { useOptimistic, useRef } from 'react';
function MessageBox({ messages, sendMessage }) {
const formRef = useRef();
// 1. オプティミスティックな状態を定義
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { text: newMessage, sending: true }]
);
async function formAction(formData) {
const message = formData.get("message");
// 2. 即座にUIを更新
addOptimisticMessage(message);
formRef.current.reset();
// 3. 実際のサーバーリクエストを実行
await sendMessage(message);
}
return (
<>
<div>
{optimisticMessages.map((m, i) => (
<div key={i} style={{ opacity: m.sending ? 0.6 : 1 }}>
{m.text} {m.sending && <small>(送信中...)</small>}
</div>
))}
</div>
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="メッセージを入力..." />
<button type="submit">送信</button>
</form>
</>
);
}
React 19がここでのライフサイクルを管理します。addOptimisticMessageを呼び出すと、UIは即座に更新されます。sendMessageアクションが完了すると、Reactは自動的に一時的な状態をサーバーからの実際のデータに置き換えます。
useOptimisticの仕組み
このフックはReactのトランジション(Transition)システムに依存しています。Server Actionなどのトランジション内で更新をトリガーすると、Reactはその「保留中(pending)」ステータスを追跡します。このフックには2つの特定の要素が必要です。
- パススルー状態(Passthrough state): 信頼できる情報源(通常は親からのpropsやstate)。
- リデューサー関数(Reducer function): 新しいデータを現在の状態にマージする純粋関数。
最大の利点は自動ロールバックです。APIが500エラーを返した場合でも、「元に戻す」関数を書く必要はありません。オプティミスティックな状態は非同期アクションのライフサイクルに紐付いているため、アクションが解決した瞬間にReactは一時的なバージョンを破棄し、最後に確認された状態に戻ります。
最近、タスク完了の動作が「遅い」という苦情があったプロジェクトにこれを導入しました。useOptimisticに切り替えただけで、インターフェースは2倍速くなったように感じられました。APIのレスポンス時間は全く変わっていませんが、ユーザーが感じるスピードの認識が完全に変わったのです。
Server Actionsとの相乗効果
標準的なイベントハンドラーでも動作がありますが、このフックはServer Actionsと組み合わせた時に真価を発揮します。ユーザーがフォームを送信すると、Reactは自動的にトランジションを開始します。useOptimisticフックはこのトランジションを検知し、サーバーが応答するまで一時的なUI変更を適用します。
高度なパターン:単純な文字列を超えて
本番環境では、単に配列に文字列を追加するだけではないケースがほとんどです。一時的なIDの管理や、処理中のアイテムに対する特定のスタイリングが必要になることがよくあります。
const [optimisticItems, addOptimisticItem] = useOptimistic(
items,
(state, newItem) => [
...state,
{
...newItem,
id: crypto.randomUUID(), // キーの衝突を防ぐ
isPending: true
}
]
);
isPendingフラグを使用することで、アイテムの外観を変更できます。半透明にしたり、「同期中」アイコンを表示したりすることが可能です。これにより、ユーザーは自分の操作が受け付けられたことを認識でき、成功の確認を待つ必要がなくなります。
エラーの優雅な処理
サーバーリクエストが失敗した場合、useOptimisticがUIの復元を処理してくれますが、ユーザーにその旨を伝える必要はあります。フックとトースト通知システムを組み合わせることで、最高の体験を提供できます。
async function handleAction(formData) {
try {
addOptimisticItem({ name: formData.get('name') });
await updateDatabase(formData);
} catch (error) {
toast.error("接続が失われました。変更は保存されませんでした。");
// ここでReactが自動的にUIをロールバックします
}
}
スムーズな体験のためのベストプラクティス
オプティミスティック更新は強力ですが、万能な解決策ではありません。UXの一貫性を保つために、以下の3つのルールを守りましょう。
1. 重大な操作には避ける
重要な金融取引やセキュリティ設定にはuseOptimisticを使用しないでください。ユーザーが口座間で5,000ドルを送金する場合、サーバーがその取引を承認したという事実を確実に知る必要があります。突然ネットワークエラーで消えてしまうような「成功」メッセージを表示することは、ユーザーの信頼を損ないます。
2. リデューサーのロジックを純粋に保つ
リデューサー関数は次の状態を計算するだけのものでなければなりません。この関数の中で副作用(サイドエフェクト)を発生させたり、API呼び出しや解析コードを実行したりしないでください。既存のデータの予測可能な変換であるべきです。
3. サーバーの挙動を再現する
オプティミスティック更新はサーバーのロジックを模倣する必要があります。バックエンドがコメントを新しい順にソートする場合、オプティミスティックなリデューサーもリストの先頭にメッセージを追加すべきです。実際のデータが届いた時にUIが飛んだり並び順が変わったりすると、遷移がスムーズではなく不快なものになってしまいます。
まとめ
React 19のuseOptimisticフックは、フロントエンド開発者にとって大きなアップグレードです。複雑な状態管理の苦労を、宣言的で管理しやすいパターンへと変えてくれます. 未来を予測することで、瞬時に反応するインターフェースを構築し、インターネット固有の遅延を効果的に隠すことができるのです。
次にコメント欄や設定の切り替えなど、サーバーとの通信が必要な機能を構築する際は、ローディングスピナーを置き換えてみてください。ユーザーはそのスピードの違いをきっと喜んでくれるはずです。

