第168章:更新後の「キャッシュ無効化」
更新ボタン押したのに、画面が古いまま…😇💦 今日はそれを一発で直す TanStack Query の「invalidate(無効化)」 をやるよ〜!💪🌟
今日のゴール🎯
- ✅ 更新(Mutation)のあとに、古いキャッシュを“古いよ!”って宣言できるようになる
- ✅ 一覧・詳細みたいに 複数のクエリをまとめて最新化できるようになる
- ✅
invalidateQueriesの「どれが対象?」がわかるようになる
TanStack Query の invalidateQueries は、該当クエリを stale(古い)扱いにして、画面で使われてたら自動で裏で再取得までしてくれるよ〜!✨
しかもこの “stale 扱い” は staleTime を上書きするのがポイント!😳💡 (TanStack)
まず図でイメージつかも!🧠📌(Mermaid)
この挙動は公式ガイドでもこう説明されてるよ👇
- stale にする(
staleTimeより強い) - 画面で使われてたら refetch も走る (TanStack)
「どのキャッシュを無効化するの?」= queryKey で決まる🏷️✨
invalidateQueries({ queryKey: ['todos'] }) って書くと、
queryKey が ['todos'] で始まるもの全部が対象になるよ!😺🧺 (TanStack)
実践:更新したら「一覧」を最新にする📝✨
1) 例の“なんちゃってサーバー”を用意(学習用)🍳
// src/fakeApi/todos.ts
export type Todo = { id: number; title: string; done: boolean };
let todos: Todo[] = [
{ id: 1, title: "レポート出す📄", done: false },
{ id: 2, title: "カフェ行く☕", done: false },
];
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function fetchTodos(): Promise<Todo[]> {
await wait(400);
return [...todos];
}
export async function updateTodo(input: { id: number; title: string }): Promise<Todo> {
await wait(400);
todos = todos.map((t) => (t.id === input.id ? { ...t, title: input.title } : t));
return todos.find((t) => t.id === input.id)!;
}
2) 一覧を表示して、更新したら invalidate!🧹✨
// src/TodoApp.tsx
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { fetchTodos, updateTodo, Todo } from "./fakeApi/todos";
import { useState } from "react";
export function TodoApp() {
const queryClient = useQueryClient();
const { data: todos = [], isPending } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
staleTime: 60_000, // わざと長め(invalidate の効果が分かりやすい✨)
});
const [editingId, setEditingId] = useState<number | null>(null);
const [title, setTitle] = useState("");
const mutation = useMutation({
mutationFn: updateTodo,
onSuccess: async () => {
// ✅ 更新が成功したら「一覧は古い!」と宣言🧹
await queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
if (isPending) return <p>読み込み中…🐣</p>;
return (
<div style={{ padding: 16 }}>
<h2>TODO一覧📝</h2>
<ul>
{todos.map((t) => (
<li key={t.id} style={{ marginBottom: 8 }}>
{t.title}{" "}
<button
onClick={() => {
setEditingId(t.id);
setTitle(t.title);
}}
>
編集✏️
</button>
</li>
))}
</ul>
{editingId !== null && (
<div style={{ marginTop: 16 }}>
<h3>編集モード✏️✨</h3>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
style={{ width: 240 }}
/>
<button
onClick={() => mutation.mutate({ id: editingId, title })}
disabled={mutation.isPending}
>
{mutation.isPending ? "保存中…💾" : "保存💾"}
</button>
<button onClick={() => setEditingId(null)} style={{ marginLeft: 8 }}>
閉じる🙈
</button>
</div>
)}
</div>
);
}
これで、保存した瞬間に ['todos'] のキャッシュが無効化 → 一覧が再取得されるよ!🔄✨
Mutation 成功時に invalidate する流れは公式ガイドでも定番パターンだよ〜! (TanStack)
一覧 + 詳細みたいに「複数」無効化したいとき🧺✨
たとえば queryKey がこうだとするね👇
- 一覧:
['todos'] - 詳細:
['todo', id]
更新後は両方古い可能性あるよね?😵💫 そんなときはこれ!
onSuccess: async (_updated, variables) => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["todos"] }),
queryClient.invalidateQueries({ queryKey: ["todo", variables.id] }),
]);
},
公式の例でも Promise.all で複数 invalidate やってるよ🧡 (TanStack)
ちょい設定:refetchType で「再取得する範囲」を変えられる🎚️✨
refetchType: 'active'(デフォ)👉 今画面で使われてるやつだけ裏で再取得refetchType: 'none'👉 stale にするだけ(通信はしない)refetchType: 'all'👉 使われてないやつも含めて再取得 (TanStack)
例:
await queryClient.invalidateQueries({
queryKey: ["todos"],
refetchType: "none",
});
さらに気持ちよく:setQueryData で「即反映」⚡(通信はあとでもOK)
「保存したらすぐ画面変わってほしい〜🥺」って時は、 invalidate だけじゃなくて キャッシュ自体を先に書き換えるのもアリ!
onSuccess: (updated) => {
queryClient.setQueryData<Todo[]>(["todos"], (old) =>
(old ?? []).map((t) => (t.id === updated.id ? updated : t))
);
},
※この更新は イミュータブル(元を直接いじらない) が大事だよ!✋💥 (TanStack)
よくあるハマりどころ😇🕳️
- ❌
queryKeyの形が違う('todos'と['todos']が混ざる、とか) - ❌ 一覧は
['todos']、別画面は['todoList']みたいに 名前が統一されてない - ❌ 無効化したいのに
exact: trueをつけてて 思ったより当たってない(ピンポイントすぎ) (TanStack)
まとめ🎀✨
- 更新したら invalidateQueries で「古いよ!」宣言する🧹
queryKeyは「住所」🏷️:一致(前方一致)したものが無効化される (TanStack)- 必要なら
Promise.allで複数まとめて invalidate 🧺 (TanStack) refetchTypeで「裏で取り直す範囲」も調整できる🎚️ (TanStack)
次の第169章は、ローディング・エラー表示を“いい感じ”にしてアプリが一気にプロっぽくなるやつだよ〜😎✨