useOptimisticでさくっと実装する楽観的更新(Optimistic Update)
はじめに
UI/UXにおける、Optimistic Updateって知っていますか?日本語にすると楽観的更新などと呼ばれたりします。実は日常にたくさん存在しており、見かけたことはあるかなと思います。
例えば、Xのいいねボタンです。「いいね」した瞬間にハートに色がつくのですが、サーバーへのリクエストとUIの更新は同時に行われています。つまりサーバーへのリクエスト結果を待たずにUIを更新しているということです。これが楽観的更新になります。
リクエスト結果を待ってからUIを更新する場合、ユーザーが「いいね」をしてからハートに色がつくまで時間がかかり、UXが悪くなってしまいます。そのため本当はリクエスト結果を待つ必要があるけど最終的にハートに色がつくだろうから先にUIを更新してしまえ!!という感じです。
そんな楽観的更新ですが、React19から「UIを楽観的に(optimistically)更新するためのReactフック」であるuseOptimisticが登場したので、その使い方をご紹介します。
useOptimisticとは?
useOptimisticは、サーバーリクエストのような非同期処理が完了する前に、UIを楽観的に更新するためのReactフックです。
const [optimisticState, addOptimistic] = useOptimistic(
state,
// updateFn
(currentState, optimisticValue) => {
// merge and return new state
// with optimistic value
}
);
引数と返り値それぞれ見ていきましょう!
引数
state
初期状態、またはアクションが実行中でない場合に返される値になります。
updateFn(currentState, optimisticValue)
アクション中に表示する値を返却する関数を設定します。また、currentState
は現在の状態の値、optimisticValue
は返り値のaddOptimistic
に渡す楽観的更新に使用する値となります。
返り値
optimisticState
アクションが実行中でない場合は引数のstate
と等しくなり、何らかのアクションが実行中の場合は updateFn()
が返す値と等しくなります。つまり、楽観的更新中はその一時的な値となり、アクション完了時には実際のアクション完了後の値を保持します。
addOptimistic
楽観的更新を行うためのdispatch関数であり、任意の型の引数optimisticValue
を受け取り、updateFn()
を通じてステートが更新されます。
useOptimisticは元の値と、追加の情報を受け取って新しい値を作るという、使い方自体はuseReducerと似ている感じがしますね!
useOptimisticを使ってみる
それでは実際にuseOptimisticを使ってみましょう。
以下はフォームのアクション中は追加したTodoにopacity
をかけ、アクションが完了したらopacity
をなくすような実装です。
コードは以下の通りで、順番にポイントを説明していきます。(スタイルの実装は削除しています。)
import { useActionState, useOptimistic } from "react";
type Todo = {
id: string;
title: string;
};
type OptimisticTodos = Todo & {
pending?: boolean;
};
const updateOptimistic = (state: OptimisticTodos[], newTodo: Todo) => [
...state,
{
id: newTodo.id,
title: newTodo.title,
pending: true,
},
];
export const TodoPage = () => {
const optimisticAction = async (
prevState: OptimisticTodos[],
formData: FormData
) => {
const newTodoId = crypto.randomUUID();
const newTodoTitle = formData.get("todo") as Todo["title"];
// 楽観的更新
addOptimisticTodos({ id: newTodoId, title: newTodoTitle });
// 何かしらのサーバーリクエストなどの非同期処理
await new Promise((resolve) => setTimeout(resolve, 500));
return [
...prevState,
{
id: newTodoId,
title: newTodoTitle,
},
];
};
const [todos, formAction] = useActionState(optimisticAction, []);
const [optimisticTodos, addOptimisticTodos] = useOptimistic(
todos,
updateOptimistic
);
return (
<div>
<form action={formAction}>
<input type="text" name="todo" />
<button>Create</button>
</form>
<ul>
{optimisticTodos.map(({ id, title, pending }) => {
return (
<li key={id} style={{ opacity: pending ? 0.2 : undefined }}>
{title}
</li>
);
})}
</ul>
</div>
);
};
1. 非同期トランジションはuseActionStateで行う
アクションはuseActionStateで管理しています。useOptimisticは非同期トランジションの中で楽観的更新をするためのフックであり、useActionStateのdispatch処理であるoptimisticAction
でステート更新を行うことで非同期トランジションとみなされ、楽観的更新を行うことができます。(今回はuseOptimisticがメインであるため、useActionStateの説明は省略します。)
2. useOptimisticで状態を管理する
const [optimisticTodos, addOptimisticTodos] = useOptimistic(
todos,
updateOptimistic
);
2-1. 引数
- 第一引数:
todos
- todosはuseActionStateより値を取得した値であり、アクション完了後の値になります。
- 第二引数:
updateOptimistic
- アクション実行中に表示する値を返却する関数であり、
pending=true
を設定し、アクションが実行中であること情報を保持します。
- アクション実行中に表示する値を返却する関数であり、
2-2. 返り値
-
optimisticTodos
- 画面表示に使用するtodosです。
-
addOptimisticTodos
- 引数で渡した
updateOptimistic
を実行するためのdispatch関数です。
- 引数で渡した
3. optimisticActionでアクションを実行する
const optimisticAction = async (
prevState: OptimisticTodos[],
formData: FormData
) => {
const newTodoId = crypto.randomUUID();
const newTodoTitle = formData.get("todo") as Todo["title"];
// 楽観的更新
addOptimisticTodos({ id: newTodoId, title: newTodoTitle });
// 何かしらのサーバーリクエストなどの非同期処理
await new Promise((resolve) => setTimeout(resolve, 500));
return [
...prevState,
{
id: newTodoId,
title: newTodoTitle,
},
];
};
-
formData
より入力したtitle
を取得する。 - 何かしらのサーバーリクエストなどの非同期処理を行う前に
addOptimisticTodos()
で楽観的更新を行う。→ このタイミングでUIが更新する。(opacityをかける) - 何かしらのサーバーリクエストなどの非同期処理を行う。(今回は
setTimeout()
で500ms待つ) - アクション完了後の実際の値を返却する。
アクション完了後にtodos
が更新され、それによって画面表示に使用しているoptimisticTodos
も更新され、楽観的更新時の値ではなくアクション完了後の値が表示されます。
最後に
useOptimisticを使うことで、楽観的更新をシンプルに実装できることが分かりました。この手法を適用することで、UXを向上させ、ユーザーにスムーズな操作感を提供できます。
楽観的更新は、いいねボタンやフォーム送信のUI改善だけでなく、さまざまなインタラクションに応用可能です。私自身も積極的に活用していこうと思います!ぜひ皆さんも、さまざまな場面でuseOptimisticを試してみてください!
それではよきReactライフを!!🎉
参考
Discussion