レビュー済み·難易度: 中級·更新: 2026-04-17

TanStack Query の useQueryClient で楽観的更新を実装する

useMutation の onMutate で UI を即時更新し、失敗時に onError でロールバックする楽観的更新の実装例。

nextjsapicrudtanstack-query

対応バージョン

nextjs 15react 19tanstack-query 5

前提環境

TanStack Query の useMutation と useQueryClient の基本を理解していること(tanstack-query-mutation を参照)

概要

楽観的更新(Optimistic Update)とは、サーバーのレスポンスを待たずに UI を即時更新するパターン。 onMutate でキャッシュを先に書き換え、onError でロールバック、onSettled でサーバーと同期する。

インストール

npm install @tanstack/react-query

実装例(いいねボタン)

// src/components/LikeButton.tsx
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query";

type Post = { id: number; title: string; likes: number };

async function likePost(postId: number): Promise<Post> {
  const res = await fetch(`/api/posts/${postId}/like`, { method: "POST" });
  if (!res.ok) throw new Error("いいねに失敗しました");
  return res.json();
}

type Props = { post: Post };

export function LikeButton({ post }: Props) {
  const queryClient = useQueryClient();

  const { mutate } = useMutation({
    mutationFn: () => likePost(post.id),

    onMutate: async () => {
      // 進行中のフェッチをキャンセルして競合を防ぐ
      await queryClient.cancelQueries({ queryKey: ["posts"] });

      // 現在のキャッシュを保存(ロールバック用)
      const previousPosts = queryClient.getQueryData<Post[]>(["posts"]);

      // キャッシュを即時更新(楽観的更新)
      queryClient.setQueryData<Post[]>(["posts"], (old) =>
        old?.map((p) => (p.id === post.id ? { ...p, likes: p.likes + 1 } : p))
      );

      return { previousPosts }; // onError に渡す
    },

    onError: (_err, _vars, context) => {
      // エラー時に以前のキャッシュに戻す
      if (context?.previousPosts) {
        queryClient.setQueryData(["posts"], context.previousPosts);
      }
    },

    onSettled: () => {
      // 成功・失敗にかかわらずサーバーと同期する
      queryClient.invalidateQueries({ queryKey: ["posts"] });
    },
  });

  return (
    <button
      onClick={() => mutate()}
      className="flex items-center gap-1 rounded border px-3 py-1 text-sm hover:bg-gray-50"
    >
      ♥ {post.likes}
    </button>
  );
}

ポイント

  • onMutateonErroronSettled の順に実行される。onSettled は成功・失敗にかかわらず呼ばれる
  • cancelQueries で進行中のフェッチをキャンセルしないと、レスポンスが楽観的更新を上書きする競合が起きる
  • onMutate の戻り値が context として onError / onSettled に渡される
  • onSettledinvalidateQueries を呼ぶことで、最終的にサーバーの値に同期される
  • ネットワーク遅延が長い場合ほど楽観的更新の UX 向上効果が大きい

注意点

onMutate でキャッシュを即時更新し、onError で以前のキャッシュに戻す。onSettled で必ず invalidateQueries を呼ぶことでサーバーとの整合性を取る。cancelQueries を忘れると進行中のフェッチと競合する場合がある。

関連サンプル