概要
楽観的更新(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 向上効果が大きい