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

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

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

nextjsapicrudtanstack-query

対応バージョン

nextjs 15react 19tanstack-query 5

前提環境

TanStack Query の useQuery と QueryClientProvider の基本を理解していること(tanstack-query-data-fetching を参照)

概要

useMutation を使うと、POST / PUT / DELETE などの副作用操作を宣言的に管理できる。 isPending / isError / isSuccess で状態を分岐でき、onSuccess で成功後のキャッシュ更新も簡潔に書ける。

インストール

npm install @tanstack/react-query

POST の実装例(アイテム追加)

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

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

type Item = { id: number; name: string };

async function addItem(name: string): Promise<Item> {
  const res = await fetch("/api/items", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name }),
  });
  if (!res.ok) throw new Error("追加に失敗しました");
  return res.json();
}

export function AddItemForm() {
  const [name, setName] = useState("");
  const queryClient = useQueryClient();

  const { mutate, isPending, isError, error } = useMutation({
    mutationFn: addItem,
    onSuccess: () => {
      // items のキャッシュを無効化して再フェッチ
      queryClient.invalidateQueries({ queryKey: ["items"] });
      setName("");
    },
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        mutate(name);
      }}
      className="flex gap-2"
    >
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        className="rounded border px-3 py-2 text-sm"
        placeholder="アイテム名"
      />
      <button
        type="submit"
        disabled={isPending || name.trim() === ""}
        className="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
      >
        {isPending ? "追加中..." : "追加"}
      </button>
      {isError && <p className="text-sm text-red-600">{error.message}</p>}
    </form>
  );
}

DELETE の実装例(アイテム削除)

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

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

type Item = { id: number; name: string };

async function fetchItems(): Promise<Item[]> {
  const res = await fetch("/api/items");
  if (!res.ok) throw new Error("取得に失敗しました");
  return res.json();
}

async function deleteItem(id: number): Promise<void> {
  const res = await fetch(`/api/items/${id}`, { method: "DELETE" });
  if (!res.ok) throw new Error("削除に失敗しました");
}

export function ItemList() {
  const queryClient = useQueryClient();
  const { data: items = [] } = useQuery({ queryKey: ["items"], queryFn: fetchItems });

  const { mutate: remove } = useMutation({
    mutationFn: deleteItem,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["items"] });
    },
  });

  return (
    <ul className="space-y-2">
      {items.map((item) => (
        <li key={item.id} className="flex items-center justify-between rounded border px-4 py-2 text-sm">
          <span>{item.name}</span>
          <button
            onClick={() => remove(item.id)}
            className="text-red-600 hover:underline"
          >
            削除
          </button>
        </li>
      ))}
    </ul>
  );
}

ポイント

  • mutate() は非同期ではない。await を使いたい場合は mutateAsync() を使う
  • onSuccess 内で queryClient.invalidateQueries() を呼ぶと、関連する useQuery のキャッシュが無効化されて自動的に再フェッチされる
  • isPendingtrue の間はボタンを disabled にして二重送信を防ぐ
  • useMutation はキャッシュキーを持たないため、useQuery とは独立して動作する

注意点

useMutation は onSuccess / onError コールバックで副作用を扱う。mutate() は非同期ではないため、await は使えない。非同期にしたい場合は mutateAsync() を使う。

関連サンプル