TOOLS BOX/ガイド/useOptimistic
API / Option

useOptimistic

サーバーのレスポンスを待たずに UI を先に更新する楽観的更新を実装する React Hook。Server Action の完了後に実際のデータで上書きされる。

reacthookoptimisticuseOptimisticserver-actionsux
所属:react

代表的な値 / 使い方

  • const [optimisticItems, addOptimisticItem] = useOptimistic(items, (state, newItem) => [...state, newItem])
  • const [optimisticCount, setOptimisticCount] = useOptimistic(likeCount, (_, next) => next)

注意点 / Pitfalls

  • ·楽観更新はサーバー確認前の仮の状態。Server Action がエラーになると元の state に戻る
  • ·Client Component でのみ使える
  • ·useOptimistic の状態は Server Action の完了後に props の最新値で自動的に上書きされる
  • ·Server Action 失敗時のロールバックは自動だが、ユーザーへのエラー表示は別途実装する必要がある

一緒に使う項目

補足

ネットワーク遅延をユーザーに感じさせないための UX 改善手段。いいねボタン・リスト追加・削除など、サーバーへの書き込みが高確率で成功する操作に向く。失敗時のロールバックは React が自動で行う。

楽観的更新とは

サーバーのレスポンスを待たずに、先に UI を更新する手法です。Server Action が完了したら実際のデータで上書きされ、失敗した場合は元の状態に自動で戻ります。

const [optimisticState, addOptimistic] = useOptimistic(
  actualState,         // 実際のデータ(Server Component から props で受け取る)
  (state, newValue) => updatedState  // 楽観的な状態を計算する関数
);

いいねボタンの例

"use client";
import { useOptimistic } from "react";
import { toggleLikeAction } from "./actions";

export function LikeButton({
  postId,
  initialCount,
  initialLiked,
}: {
  postId: string;
  initialCount: number;
  initialLiked: boolean;
}) {
  const [optimistic, setOptimistic] = useOptimistic(
    { count: initialCount, liked: initialLiked },
    (state) => ({ count: state.liked ? state.count - 1 : state.count + 1, liked: !state.liked })
  );

  async function handleClick() {
    setOptimistic(undefined); // 楽観的に先に UI を更新
    await toggleLikeAction(postId);
  }

  return (
    <button onClick={handleClick}>
      {optimistic.liked ? "❤️" : "🤍"} {optimistic.count}
    </button>
  );
}

リスト追加の例

"use client";
import { useOptimistic, useRef } from "react";
import { addCommentAction } from "./actions";

export function CommentForm({ postId, comments }: { postId: string; comments: Comment[] }) {
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (state, newComment: Comment) => [...state, newComment]
  );
  const ref = useRef<HTMLFormElement>(null);

  async function handleSubmit(formData: FormData) {
    const text = formData.get("text") as string;
    addOptimisticComment({ id: crypto.randomUUID(), text, pending: true });
    ref.current?.reset();
    await addCommentAction(postId, text);
  }

  return (
    <>
      <ul>
        {optimisticComments.map((c) => (
          <li key={c.id} style={{ opacity: c.pending ? 0.5 : 1 }}>
            {c.text}
          </li>
        ))}
      </ul>
      <form ref={ref} action={handleSubmit}>
        <input name="text" required />
        <button type="submit">送信</button>
      </form>
    </>
  );
}

失敗時の挙動

Server Action がエラーになると、楽観的に更新した UI は自動で元の状態(actualState)に戻ります。ただし、ユーザーへのエラー表示は別途実装が必要です。

async function handleSubmit(formData: FormData) {
  addOptimisticItem(newItem); // 先に UI を更新
  try {
    await createItemAction(formData); // サーバー確認
  } catch (e) {
    // エラー時: useOptimistic の状態は自動ロールバックされる
    // ユーザーへの通知は自前で行う
    alert("失敗しました");
  }
}

向いている場面 / 向かない場面

向いている向かない
いいね・フォロー(成功率が高い操作)決済・送金など失敗時のリカバリが複雑な操作
コメント追加・削除サーバーが返す値に依存する操作(採番 ID など)
表示非表示の切り替えエラー時に丁寧なロールバック UI が必要な場面

送信状態の管理には → useActionState

Server Actions の使い方 → Server Actions

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例