概要
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のキャッシュが無効化されて自動的に再フェッチされるisPendingがtrueの間はボタンをdisabledにして二重送信を防ぐuseMutationはキャッシュキーを持たないため、useQueryとは独立して動作する