© TOOLS BOX — Next.js / React / TypeScript コードサンプル集

サンプルガイド
←サンプル一覧
nextjsapi

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

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

難易度: 中級·更新: 2026-04-17·
tanstack-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>
  );
}

ポイント

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

注意点

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

関連サンプル

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

  • TanStack Query の useMutation で POST / DELETE を管理する

    useMutation を使い、フォーム送信・削除などの副作用操作を型安全に管理する実装例。成功・エラー・ローディング状態を宣言的に扱う。

  • Next.js Route Handler で REST API(CRUD)を実装する

    app/api/ 配下の Route Handler を使い、GET / POST / PUT / DELETE を備えた REST API を実装する例。インメモリデータを使ってシンプルに CRUD を示す。

  • Prisma と Route Handler で CRUD API を実装する

    Prisma Client の findMany / create / update / delete を Route Handler から呼び出して CRUD API を構築する例。スキーマ定義からマイグレーション・型安全なクエリまでを示す。

  • Prisma の orderBy で並び替えクエリを実装する

    Prisma の orderBy による単一フィールド・複数フィールド・リレーション先フィールドでの並び替えパターンと、null 値の扱い(nulls first / last)の例。

  • TanStack Query の useInfiniteQuery で無限スクロールを実装する

    useInfiniteQuery を使い、スクロールに応じて次ページを自動フェッチする無限スクロールの実装例。Intersection Observer と組み合わせる。

関連仕様

このサンプルを理解するのに役立つ仕様や概念

  • FrameworkNext.jsReact ベースのフルスタックフレームワーク。SSR・SSG・App Router・API Routes を提供する。
←サンプル一覧に戻る