Optimistic Updateとは
Optimistic Updateは、ウェブアプリケーションやモバイルアプリケーションの開発において使われる手法の一つで、ユーザーが行った操作をサーバーからのレスポンスを待たずにUIに反映します。
Optimistic Updateと比較して、従来の手法は、悲観的更新(Pessimistic Update)と呼ばれたりします。
これは、ユーザーが行った操作に対するフィードバックはサーバーからの応答を受け取ってから行われます。
Optimistic Updateの代表的な特徴として、以下の3つが挙げられます。
特徴1:即時のフィードバック
ユーザーがデータの作成、更新に関わる処理を行ったとき、サーバーへのリクエストを待たずに、UI上でその変更を即時に反映します。
ユーザーの待機時間をゼロにすることで、ユーザー体験の向上を見込めます。
特徴2:バックグラウンドで処理を実行
UI上でフィードバックをすると同時に、データの更新をバックグラウンドで非同期に行います。
特徴3:エラーハンドリング
サーバーでの処理に失敗した場合(ネットワークエラーやデータの不整合など)、即時フィードバックによるUI上の変更を元に戻し、エラーが発生したことをユーザーに伝えます。
メリット
即時フィードバックによるユーザー体験の向上
Optimistic Updateだと、ユーザーのネットワーク環境やバックエンドの処理にかかる時間に関わらず、ユーザーが行った操作をすぐにUIに反映することができるため、よりスムーズな体験を提供することができます。
デメリット
エラー時の設計がわかりづらくなりやすい
一般的に、ユーザーのアクションに従ったUIの反映がされた場合、ユーザーは「正常に処理をされた」と認識するため、エラー時のフィードバックが余計な混乱を招く可能性があります。
そのため、エラー時の挙動の設計に関しては、悲観的更新と比べてわかりやすい体験を提供する難易度は高いと感じます。
一時的なデータの不一致が発生するリスクがある
バックエンドの処理が終わる前に、UI上は更新された画面をユーザーが操作できる状態になるため、処理が長時間かかる場合は、データの不整合によるバグが発生するリスクが高くなります。
この場合は、画面には反映しつつも、バックエンド側の処理が終わるまで非活性にするなどの工夫が必要です。
Optimistic Updateの最適な場面
Optimistic Updateが適しているUIの事例を紹介します。
1. 「いいね」ボタン
「いいね」ボタンをクリックした場合に、クリックと同時に「いいね」をした状態になりますが、実際の更新はバックグラウンドでサーバーに送信され、処理されます。
もしサーバー側で処理が失敗した場合は、「いいね」していない状態に戻ります。
2. モード切り替えによって入力可能になるフォーム
モバイルアプリでよく見られるUIですが、文字列をタップすると入力モードに切り替わり、フォーカスが外れると通常のモードになり、更新処理が行われます。
このUIは、エラーが発生したらロールバックするという設計がユーザーから受け入れやすいため、親和性が高いです。
3. トグルスイッチ
トグルスイッチは、ユーザーのアクションを即時フィードバックさせる必要があり、楽観的変更との相性が抜群です。
React Queryでは、ソリューションとしてuseuMutationState
というフックも用意しているので、うまく組み合わせることをおすすめします。
実装例
以下、React Queryとswrを用いたOptimistic Updateの実装例です。
React Query
const queryClient = useQueryClient()
useMutation({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
// Return a context object with the snapshotted value
return { previousTodos }
},
// If the mutation fails,
// use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
swr
const { data, mutate } = useSWR("/api/todos", getTodos);
await mutate(addTodo(newTodo), {
optimisticData: [...data, newTodo],
rollbackOnError: true,
populateCache: true,
revalidate: false
});
最後に
Optimistic Updateはユーザー体験向上の武器となりつつ、例外が発生時の設計を設計段階で行わないと、ユーザーが混乱する根源となる可能性があります。
弊社プロダクトの「Musubite」でもOptimistic Updateを用いた機能をリリース予定なので、ご期待ください!
カジュアルトークも可能ですので、気になった方はぜひお気軽にお話ししましょう🔥