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 です。ここを理解するとキャッシュ挙動が一気に読めるようになります。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。