7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React】useOptimisticで楽観的更新をしよう

Last updated at Posted at 2025-11-10

はじめに

こんにちは!新卒一年目Webエンジニアの伊藤です!
今回はReact 19で導入されたuseOptimisticを紹介していきます。
初めての記事執筆のため、拙い点もあるかと思いますが、お手柔らかにお願いします。

楽観的更新とは?

楽観的更新とは、サーバーの応答を待たずに、先にUIを更新する手法です。
いいねボタンを例に挙げると、
サーバからの応答を待ってからUIを更新する場合、ユーザーがボタンを押してからハートが赤くなり、いいね数が増えるまで時間がかかってしまいUXが悪くなってしまいます。
そこで、本来は応答を待たなくてはいけないけどいいねボタンは押されたので最終的には更新されるのでUIだけ先に更新してしまおう!という感じです。

Videotogif (1).gif

従来の方法

1. ユーザーがいいねボタンをクリック
2. ローディング表示
3. サーバーからの応答を待つ
4. 成功したらUIを更新

楽観的更新

1. ユーザーがいいねボタンをクリック
2. 即座にUIを更新
3. バックグラウンドでサーバーに送信
4. 失敗したら元に戻す

useOptimisticとは

useOptimistic は、UI を楽観的に (optimistically) 更新するための React フックです。(公式ドキュメントより)

useOptimisticフックは、非同期処理が進行中の間、一時的に実際の状態とは異なる楽観的な状態を管理することを可能にします。

では実際にuseOptimisticの引数や返り値を見ていきましょう

const [optimisticState, addOptimistic] = useOptimistic(
    state,
    // updateFn
    (currentState, optimisticValue) => {
      // merge and return new state
      // with optimistic value
    }
  );
}

上記は公式ドキュメントのサンプルコードになります。

引数

  • state: 初期状態や、実行中のアクションが存在しない場合に返される値。
  • updateFn(currentState, optimisticValue): state の現在値と、addOptimistic に渡された楽観的更新に使用する値 (optimistic value) を受け取り、結果としての楽観的 state を返す関数。

と、公式ドキュメントに記載があります。
要約すると、

  • 第1引数:初期状態となる値
  • 第2引数:現在の状態と楽観的な値を受け取って、新しい楽観的な状態を返す更新関数

返り値

useOptimisticは2つの要素を持つ配列を返します:

  1. optimisticState: 現在の楽観的な状態。アクションが実行中の場合はupdateFnによって計算された値、そうでない場合はstateと同じ値
  2. addOptimistic: 楽観的な更新をトリガーする関数。任意の型の引数を1つ受け取り、その値をoptimisticValueとしてupdateFnに渡す

動作の仕組み

  1. 楽観的更新の開始: addOptimisticを呼び出すと、即座にUIが更新される
  2. 実際の更新処理: バックグラウンドで実際のデータ更新(API呼び出しなど)が実行される
  3. 状態の同期: 実際の更新が完了すると、stateが更新され、楽観的な状態は自動的にリセットされる

実際の使用例

いいねボタンを例に実装しました。
ハートはアイコンライブラリを使用しています

以下がいいねボタンの実装になります。
親から記事idいいねの状態いいね数いいねを更新する関数を受け取っています

// src/components/LikeButton.tsx

import { useOptimistic, startTransition } from "react";
import { Heart } from "lucide-react";

interface LikeButtonProps {
  postId: string;
  liked: boolean;
  likeCount: number;
  onLikeUpdate: (postId: string, newLiked: boolean) => Promise<void>;
}

export const LikeButton = ({
  postId,
  liked,
  likeCount,
  onLikeUpdate,
}: LikeButtonProps) => {
  const likeAction = () => {
    const currentOptimisticLiked = optimisticLike.liked;
    const newLiked = !currentOptimisticLiked;

    startTransition(async () => {
      addOptimisticLike(newLiked);
      await onLikeUpdate(postId, newLiked);
    });
  };

  const [optimisticLike, addOptimisticLike] = useOptimistic(
    { liked, likeCount },
    (state, newLiked: boolean) => ({
      liked: newLiked,
      likeCount: newLiked ? state.likeCount + 1 : state.likeCount - 1,
    })
  );

  return (
    <button
      onClick={likeAction}
      className={`
        ${
          optimisticLike.liked
            ? "bg-red-50 border-red-500 text-red-600 hover:bg-red-100"
            : "bg-gray-50 border-gray-300 text-gray-600 hover:bg-gray-100"
        }
      `}
    >
      <Heart
        className={`${optimisticLike.liked ? "fill-red-500 text-red-500" : "text-gray-500"}`}
      />
      <span className="font-medium">{optimisticLike.likeCount}</span>
    </button>
  );
}

そして以下がいいねを実際に更新する関数です。
記事idといいね状態のbooleanを受け取っています。
擬似的に処理に5秒かかるようにしています

  const updatePostLike = async (
    postId: string,
    newLiked: boolean
  ): Promise<void> => {
    await new Promise((resolve) => setTimeout(resolve, 5000)); //5秒待機させる
    startTransition(() => {
    // 投稿の情報を更新
      setPosts((currentPosts) =>
        currentPosts.map((post) =>
          post.id === postId
            ? {
                ...post,
                liked: newLiked,
                likeCount: newLiked ? post.likeCount + 1 : post.likeCount - 1,
              }
            : post
        )
      );
    });
  };

ポイント

const [optimisticLike, addOptimisticLike] = useOptimistic(
    { liked, likeCount },
    (state, newLiked: boolean) => ({
      liked: newLiked,
      likeCount: newLiked ? state.likeCount + 1 : state.likeCount - 1,
    })
  );

optimisticLikeからいいねの状態、いいね数が取得できます。
addOptimisticLike()を実行することでoptimisticLikeが更新されます。

const likeAction = () => {
    const currentOptimisticLiked = optimisticLike.liked; //現在の状態
    const newLiked = !currentOptimisticLiked; //新しい状態

    startTransition(async () => {
      addOptimisticLike(newLiked); // UIの更新
      await onLikeUpdate(postId, newLiked); //実際にいいねを更新している処理(5秒かかる)
    });
  };

buttonをクリックした際の処理で親から渡ってきているawait onLikeUpdateは本来5秒かかる処理ですが、
ご覧の通りaddOptimisticLike(newLiked)で先にUIを更新しているため、5秒待たずしてユーザーに反映を伝達することができています。

Videotogif.gif

最後に

今回はReact 19で新しく追加されたuseOptimisticフックについて紹介しました。

従来は自前で実装する必要があった楽観的更新のロジックが、このフック一つで簡潔に書けるようになりました。

useOptimisticが特に有効なケース

  • いいねボタンやブックマーク
  • コメントの投稿
  • TODOリストのチェック
  • カートへの商品追加

など、ユーザーの操作に対して即座にフィードバックを返したい場面で威力を発揮します。
積極的に活用してUXを上げていきましょう!

最後まで読んでいただき、ありがとうございました!
もし記事が参考になりましたら、いいねや共有をしていただけると嬉しいです🎉

参考

掲載したポスト

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?