Zenn
Gemcook Tech Blog
🥸

useOptimisticでさくっと実装する楽観的更新(Optimistic Update)

2025/03/25に公開
2

はじめに

UI/UXにおける、Optimistic Updateって知っていますか?日本語にすると楽観的更新などと呼ばれたりします。実は日常にたくさん存在しており、見かけたことはあるかなと思います。

例えば、Xのいいねボタンです。「いいね」した瞬間にハートに色がつくのですが、サーバーへのリクエストとUIの更新は同時に行われています。つまりサーバーへのリクエスト結果を待たずにUIを更新しているということです。これが楽観的更新になります。

リクエスト結果を待ってからUIを更新する場合、ユーザーが「いいね」をしてからハートに色がつくまで時間がかかり、UXが悪くなってしまいます。そのため本当はリクエスト結果を待つ必要があるけど最終的にハートに色がつくだろうから先にUIを更新してしまえ!!という感じです。

そんな楽観的更新ですが、React19から「UIを楽観的に(optimistically)更新するためのReactフック」であるuseOptimisticが登場したので、その使い方をご紹介します。

useOptimisticとは?

https://ja.react.dev/reference/react/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の説明は省略します。)

https://ja.react.dev/reference/react/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,
      },
    ];
  };
  1. formDataより入力したtitleを取得する。
  2. 何かしらのサーバーリクエストなどの非同期処理を行う前にaddOptimisticTodos()で楽観的更新を行う。→ このタイミングでUIが更新する。(opacityをかける)
  3. 何かしらのサーバーリクエストなどの非同期処理を行う。(今回はsetTimeout()で500ms待つ)
  4. アクション完了後の実際の値を返却する。

アクション完了後にtodosが更新され、それによって画面表示に使用しているoptimisticTodosも更新され、楽観的更新時の値ではなくアクション完了後の値が表示されます。

最後に

useOptimisticを使うことで、楽観的更新をシンプルに実装できることが分かりました。この手法を適用することで、UXを向上させ、ユーザーにスムーズな操作感を提供できます。

楽観的更新は、いいねボタンやフォーム送信のUI改善だけでなく、さまざまなインタラクションに応用可能です。私自身も積極的に活用していこうと思います!ぜひ皆さんも、さまざまな場面でuseOptimisticを試してみてください!

それではよきReactライフを!!🎉

参考

https://ja.react.dev/reference/react/useOptimistic

https://ja.react.dev/reference/react/useActionState

https://zenn.dev/uhyo/books/react-19-new/viewer/use-optimistic

2
Gemcook Tech Blog
Gemcook Tech Blog

Discussion

ログインするとコメントできます