useOptimistic を使った超高応答性 UI の構築
Olivia Novak
Dev Intern · Leapcell

はじめに:即時フィードバックでユーザーエクスペリエンスを向上させる
ペースの速い Web アプリケーションの世界では、ユーザーエクスペリエンスが最優先されます。シームレスで応答性の高いインターフェースは、もはや贅沢ではなく、基本的な期待です。しかし、投稿に「いいね!」する、カートに商品を追加する、フォームを送信するなど、サーバーサイドの処理を必要とするデータとのやり取りで最も一般的な摩擦点が生じます。UI がサーバーからの確認を待つ間のわずかな遅延は、全体的なユーザーエクスペリエンスを損なう、遅さの認識につながる可能性があります。ここで、「楽観的な更新」の概念が光ります。サーバーからの応答を待つのではなく、操作が成功すると楽観的に仮定して、UI を即座に更新します。失敗した場合は、変更を正常に元に戻します。このアプローチは、応答性とユーザー満足度を劇的に向上させます。React の新しい useOptimistic フックは、このような楽観的な更新を実装するための強力かつエレガントな方法を提供します。この記事では、真に即時感のある UI を構築するために、それをどのように活用するかを深く掘り下げていきます。
楽観的な更新と useOptimistic フックの理解
実際の実装に入る前に、いくつかのコアコンセプトを明確にしましょう。
楽観的な更新: 前述のように、楽観的な更新は、サーバーが成功を確認する前に、ユーザーの操作時に操作の視覚的な効果が即座に適用される UI パターンです。これにより、ユーザーは即座にフィードバックを得られ、アプリケーションはより高速で応答性が高くなります。サーバーが最終的にエラーを返した場合、UI は以前の状態に元に戻されます。
保留中の状態: これは、楽観的な更新が適用された後、サーバーが応答する前の UI の一時的な状態を指します。この期間中、UI は操作の期待される結果を反映します。
元に戻すメカニズム: 楽観的な更新の重要な部分は、サーバー操作が失敗した場合に UI を元の状態に元に戻す機能です。これにより、データの整合性が保証され、誤解を招く情報が防止されます。
React 18 で導入された useOptimistic フック(現在は実験的な API)は、これらの楽観的な更新を容易にするために特別に設計されています。これにより、2 つの状態、つまり現在の実際の状態と楽観的に更新された状態を維持できます。楽観的な更新をトリガーすると、useOptimistic は、現在の状態と意図されたアクションに基づいて「保留中の状態」を計算する方法を提供し、それが UI に即座に反映されます。バックグラウンド操作が完了すると、実際の状態が更新され、useOptimistic は保留中の状態を自動的に解決します。
簡単なコメントセクションで、ユーザーがコメントに「いいね!」できる例でその使用方法を説明しましょう。
import React, { useState, useOptimistic } from 'react'; interface Comment { id: string; text: string; likes: number; likedByUser: boolean; } // API 呼び出しをシミュレート const likeCommentApi = async (commentId: string, isLiking: boolean): Promise<Comment> => { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.1) { // 90% の成功率 resolve({ id: commentId, text: 'This is a sample comment.', likes: isLiking ? 101 : 100, // 実際のいいねをシミュレート likedByUser: isLiking, }); } else { reject(new Error('Failed to like/unlike comment.')); } }, 500); // ネットワーク遅延をシミュレート }); }; function CommentSection() { const initialComment: Comment = { id: 'c1', text: 'This is a sample comment.', likes: 100, likedByUser: false, }; const [comment, setComment] = useState<Comment>(initialComment); // useOptimistic は [optimisticState, setOptimisticState] を返します // optimisticState は、楽観的な更新が適用された可能性のある現在の状態です。 // setOptimisticState は、楽観的な更新をトリガーするために使用されます。 const [optimisticComment, addOptimisticComment] = useOptimistic( comment, (currentComment, newLikedState: boolean) => { // この関数は保留中の状態を決定します。 // currentComment は実際の状態、newLikedState は addOptimisticComment からのペイロードです。 return { ...currentComment, likes: newLikedState ? currentComment.likes + 1 : currentComment.likes - 1, likedByUser: newLikedState, }; } ); const handleLike = async () => { const newLikedState = !comment.likedByUser; // UI を楽観的に即座に更新します addOptimisticComment(newLikedState); try { const updatedComment = await likeCommentApi(comment.id, newLikedState); // API 呼び出しが成功したら、実際の状態を更新します。 // これにより、楽観的な状態が自動的に解決されます。 setComment(updatedComment); } catch (error) { console.error('API Error:', error); // API 呼び出しが失敗した場合、UI は自動的に元に戻ります // setComment をエラーで呼び出さなかったためです。 // より複雑な元に戻し、または特定のderrorメッセージについては、 // より洗練されたエラー状態管理が必要になる場合があります。 alert('Failed to update like status. Please try again.'); // ここでの一般的なパターンは、UI を元に戻すことでもあります // 楽観的な更新の前にあった状態に実際の状態を設定することによって。 // ただし、useOptimistic は、失敗後に setComment が呼び出されない場合、これを暗黙的に処理します。 // `setComment` が新しいデータで呼び出されない場合、`optimisticComment` は自然に `comment` にフォールバックします。 } }; return ( <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}> <p>{optimisticComment.text}</p> <p>Likes: {optimisticComment.likes}</p> <button onClick={handleLike} disabled={false}> {optimisticComment.likedByUser ? 'Unlike' : 'Like'} </button> {optimisticComment.likedByUser !== comment.likedByUser && ( <span style={{ marginLeft: '10px', color: 'gray' }}> (Updating...)</span> )} </div> ); } // App.tsx または同様のファイルで: // function App() { // return <CommentSection />; // }
この例では、
useStateを使用してcomment状態を初期化します。これが私たちの実際の真実の情報源です。- 次に、
comment状態とリデューサー関数でuseOptimisticを初期化します。- リデューサー 
(currentComment, newLikedState)は、現在の実際の状態 (currentComment) とaddOptimisticCommentに渡されたペイロード (newLikedState) を受け取ります。 - これは、UI に即座に表示される新しい楽観的な状態を返します (
optimisticComment)。この場合、いいねの数を増減させ、likedByUserを切り替えます。 
 - リデューサー 
 handleLikeが呼び出されると、最初にaddOptimisticComment(newLikedState)を呼び出します。これにより、API 呼び出しが開始される前に、optimisticCommentが即座に更新され、UI が「いいね!」された外観で再レンダリングされます。- 次に、 
likeCommentApiが非同期に呼び出されます。 - API 呼び出しが成功すると、
setComment(updatedComment)が呼び出されます。これにより実際の状態が更新され、useOptimisticは自動的に同期され、optimisticCommentが正常に更新されたcommentを反映していることを保証します。 - API 呼び出しが失敗すると、 
setCommentは呼び出されません。実際comment状態が変更されていないため、optimisticCommentは効果的に最後の安定したcomment状態に元に戻り、UI が元に戻されます。また、ユーザーにアラートを表示します。 
このパターンにより、UI は即座に更新されるため、like ボタンは非常に高速に感じられます。ユーザーは変更を確認するためにネットワークリクエストの完了を待つ必要がありません。
考慮事項とベストプラクティス
- エラーハンドリング: 
useOptimisticは、実際の状態が更新されない場合、元に戻しを暗黙的に処理しますが、堅牢なアプリケーションにはより明示的なエラーハンドリングが必要です。操作が失敗したときに、ユーザーに明確なフィードバックを提供します。 - 冪等性: 楽観的な更新は、冪等な操作(初期適用以降の結果を変更せずに複数回適用できる操作)で最も効果的です。いいね/いいね解除は良い例です。
 - 複雑なフォーム: 複数のフィールドを持つより複雑なフォームの場合、保留中のフォームデータ形状全体を 
addOptimisticCommentに渡すか、より洗練されたリデューサーを使用することができます。 - ローディングインジケーター: 楽観的な更新でも、バックグラウンド操作が進行中であることを微妙に示す(例:かすかなローディングスピナーや「更新中...」テキスト、例に示すように)のは良い習慣です。これは、操作が通常より長くかかる場合や失敗した場合に、ユーザーの期待を管理します。
 - サーバーサイド再検証: 楽観的な更新が成功した後、関連するデータをサーバーから再検証するか、ローディング状態(キャッシュされたデータなど)が正しく更新されていることを確認して、古いデータの問題を防ぐことを検討してください。
 
useOptimistic フックは、従来複雑であった楽観的な UI 更新の管理タスクを簡素化し、状態管理の多くの定型処理を抽象化します。これにより、開発者は、流動的で魅力的なユーザーエクスペリエンスの提供に集中できます。
結論:超応答性インターフェースへの飛躍
useOptimistic フックは、超応答性の高い React アプリケーションの構築における大きな一歩を表しています。開発者が比較的簡単に楽観的な UI 更新を実装できるようにすることで、ユーザーの速度と応答性の認識を根本的に変えます。楽観的な更新、特に useOptimistic によって提供される構造化されたアプローチを採用することは、即時的で、楽しく、真にユーザーを第一に考えたモダンな Web 体験を構築するために不可欠です。このフックにより、ユーザーアクションとサーバー確認の間のギャップを埋めることができ、大幅にスムーズで魅力的なインタラクションフローにつながります。

