TanStack Query のキャッシュ戦略と staleTime 入門
概要
React でサーバーの状態(サーバーから取ってくるデータ)を扱うとき、useState と useEffect で自前実装すると、ローディング・エラー管理・再取得・キャッシュ・重複リクエストの排除などを毎回書くことになります。TanStack Query(旧 React Query)はこれらをまとめて引き受けてくれるライブラリです。
その中でも初学者が最初につまずくのが staleTime(ステイルタイム)と gcTime(旧 cacheTime)の違いです。この記事ではキャッシュの状態遷移を軸に、なぜ「取得したばかりなのに再フェッチが走る」のかを整理します。対象は TanStack Query v5 です。
環境
- React 18
- TanStack Query v5(
@tanstack/react-query) - TypeScript
npm install @tanstack/react-query
前提:QueryClient のセットアップ
まずアプリのルートで QueryClientProvider を設定します。
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
}
基本的なデータ取得は useQuery で書きます。
import { useQuery } from "@tanstack/react-query";
function Todos() {
const { data, isPending, isError } = useQuery({
queryKey: ["todos"],
queryFn: () => fetch("/api/todos").then((res) => res.json()),
});
if (isPending) return <p>Loading...</p>;
if (isError) return <p>Error</p>;
return (
<ul>
{data.map((t: { id: number; title: string }) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
);
}
queryKey はキャッシュを一意に識別するキーです。同じキーの useQuery は同じキャッシュを共有します。
発生しがちな問題
デフォルト設定のまま使うと、こんな挙動に「なぜ?」となりがちです。
- タブを切り替えてブラウザに戻ってくるたびにリクエストが飛ぶ
- 別のコンポーネントがマウントされるたびに同じデータを取り直す
- 「さっき取ったばかりなのに」また API を叩いている
これは TanStack Query のデフォルトが staleTime: 0 だからです。取得直後からデータは「stale(古い)」とみなされ、再取得のトリガー(フォーカス復帰・再マウントなど)が来ると裏で再フェッチが走ります。
原因:staleTime と gcTime は別物
名前は似ていますが役割が違います。
| 設定 | 意味 | デフォルト |
|---|---|---|
staleTime |
データが「新鮮(fresh)」でいられる時間。この間は再取得しない | 0 |
gcTime |
使われなくなったキャッシュをメモリから捨てるまでの時間 | 5分(300000ms) |
キャッシュされたデータは次の状態を持ちます。
- fresh(新鮮):
staleTime内。再取得のトリガーが来ても再フェッチしない - stale(古い):
staleTimeを過ぎた状態。トリガーが来ると裏で再フェッチする(その間も古いデータは表示され続ける)
そして「そのクエリを使っているコンポーネントが1つもなくなった」状態を inactive と呼び、inactive になってから gcTime が経過するとキャッシュ自体が破棄されます。
ざっくり言うと:
staleTime= いつ取り直すかを決めるgcTime= いつ捨てるかを決める
この2つを混同すると「gcTime を伸ばしたのに再フェッチが止まらない」といった噛み合わない対処をしてしまいます。止めたいのは staleTime の方です。
対応方法
1. staleTime を設定して無駄な再取得を止める
頻繁に変わらないデータ(マスタデータ、ユーザープロフィールなど)は staleTime を長めに設定します。
const { data } = useQuery({
queryKey: ["categories"],
queryFn: fetchCategories,
staleTime: 1000 * 60 * 5, // 5分間は fresh 扱い=再取得しない
});
これで5分以内のフォーカス復帰や再マウントでは API を叩かなくなります。
2. 全体のデフォルトを変える
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 全クエリのデフォルトを1分に
},
},
});
個別の useQuery で staleTime を渡せばそちらが優先されます。
3. 再取得トリガーを個別に制御する
useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
refetchOnWindowFocus: false, // フォーカス復帰での再取得を止める
refetchOnMount: false, // 再マウント時の再取得を止める
});
まず staleTime で fresh の範囲を決め、それでも足りない細かい挙動をトリガーで調整する、という順で考えると整理しやすいです。
実装例:更新後にキャッシュを無効化する
データを更新(mutation)したあとは、古いキャッシュを stale にして取り直させます。invalidateQueries を使います。
import { useMutation, useQueryClient } from "@tanstack/react-query";
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (title: string) =>
fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ title }),
}),
onSuccess: () => {
// ["todos"] を stale にして再取得させる
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
return (
<button onClick={() => mutation.mutate("新しいタスク")}>
追加
</button>
);
}
invalidateQueries は staleTime の設定に関わらず、対象のクエリを強制的に stale にして再フェッチを促します。「更新したのに一覧が古いまま」を防ぐ定番の使い方です。
確認方法
挙動を目で確認するには公式の Devtools が便利です。
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// Provider の中に置く
<ReactQueryDevtools initialIsOpen={false} />
各クエリが fresh / stale / inactive のどれかが色分けで見えます。staleTime を変えて、fresh でいる時間がどう変わるかを確認すると理解が早いです。
注意点
staleTime: Infinityにすると自動では二度と取り直さなくなります。手動でinvalidateQueriesを呼ぶ運用が前提になります。staleTimeを長くしすぎると「古いデータが表示され続ける」リスクとのトレードオフになります。データの更新頻度に合わせて決めます。- v5 で
cacheTimeはgcTimeに改名されました。古い記事のコード例ではcacheTimeになっていることがあるので読み替えが必要です。 staleTimeとgcTimeは独立です。gcTimeを伸ばしても再取得は止まりません。止めたいのはstaleTimeです。
まとめ
staleTime= データが fresh でいる時間(=いつ取り直すか)gcTime= 未使用キャッシュを捨てるまでの時間(=いつ捨てるか)- デフォルトは
staleTime: 0なので、何もしないと再取得が頻発する - 変わりにくいデータは
staleTimeを長めに、更新後はinvalidateQueries - Devtools で状態遷移を見ながら調整するのが一番の近道
「取ったばかりなのに再フェッチする」の答えはほぼ staleTime: 0 です。ここを理解するとキャッシュ挙動が一気に読めるようになります。
