TanStack Query の stale / cache 動作を動かしながら理解する

React
Reactフロントエンド

前回の記事で TanStack Query に入門して基本的な使い方を知りましたが、この記事では stale / cache の動きについて試行した結果も踏まえて解説です。

動作環境は以下の続きにしています。

ここではキャッシュの動きを理解するために、以下の単位で記載しています。
・フェッチしたデータの鮮度(stale(古い)or fresh (新しい))
・自動フェッチのタイミング
・キャッシュの有効期限

フェッチしたデータの鮮度

デフォルトでは即 stale (古い)扱い

useQuery でクエリすると、キャッシュしたデータは stale (古い)とみなされる。

前回作ったコードで見てみます。
最初、画面表示にはリクエストを投げ、取得をしているのがわかります。
devtool でも stale になっていますね。

取得直後のデータはデフォルトでは stale
取得直後のデータはデフォルトでは stale

その後、別のタブにフォーカスを変えて、再度このタブに戻って来た場合、戻ってきタイミングに合わせてもう一度リクエストが飛んでいます。
画面上では分かりにくいですが、Tanstak Query の Github Star をつけました。そのため、2回目の取得結果が反映されたので、star の部分が 32292 → 32293 に更新されています。ちゃんと反映されています。

stale なのでデータが更新された例。query の star 数が増えて(更新されて)います
stale なのでデータが更新された例。query の star 数が増えて(更新されて)います

staleTime で調整可能

stale になるまでの時間は staleTime で設定できます(ms)。
staleTime のデフォルト値は 0(冒頭の通り取得した時点で古い)です。そのため、上の例ではタブを切り替えて戻るたびにリクエストが飛びます。

staleTime を設定します(今回個別の useQuery に指定しています)。5s です。

  const { isLoading, isFetching, isError, data, error, refetch } =
    useQuery<RepositoriesResponse>({
      queryKey: ['orgs', { key }],
      queryFn: getReposFn,
      staleTime: 1000 * 5,
    })

分かりにくいですが、取得直後は devtool で 「fresh」 の状態です。5s 後に「stale」になります。
また、「fresh」の状態でタブを変えて戻ってきてもリクエストは投げられません。

青(fetching)中から、取得後は 緑(fresh)となり、5秒後に黄(stale)になっています
青(fetching)中から、取得後は 緑(fresh)となり、5秒後に黄(stale)になっています

自動再フェッチのタイミング

公式には以下の様に書かれています。

Stale queries are refetched automatically in the background when:
 New instances of the query mount
 The window is refocused
 The network is reconnected
 The query is optionally configured with a refetch interval

https://tanstack.com/query/v4/docs/react/guides/important-defaults

上のそれぞれのタイミングは、各種設定で変更が可能(以下)。実際には boolean以外に関数が渡せたりします。詳細は公式を参照ください。

  const { isLoading, isFetching, isError, data, error, refetch } =
    useQuery<RepositoriesResponse>({
      queryKey: ['orgs', { key }],
      queryFn: getReposFn,
      // refetchOnMount: false,
      // refetchOnWindowFocus: false,
      // refetchOnReconnect: false,
      // refetchInterval: 1000,
      // staleTime: 1000 * 5,
    })
タイミング設定項目デフォルト値備考
クエリの新しいインスタンスがマウントされたときrefetchOnMounttrue
ウィンドウに再度フォーカスがあたったときrefetchOnWindowFocustrue
ネットワークが再接続したときrefetchOnReconnecttrue
オプションで設定したリフェッチのインターバルrefetchIntervalfalsemsで数値指定するとその間隔で再取得
データがリフェッチされるタイミング

キャッシュしたデータの有効期限

上のように一度キャッシュしたデータの有効期限も設定が可能です。
結論としては、設定は以下のように、これまでと同じように cacheTime という値を ms で指定します。デフォルトは 5分で、非アクティブなクエリに対するキャッシュはガベージコレクトされます。

  const { isLoading, isFetching, isError, data, error, refetch } =
    useQuery<RepositoriesResponse>({
      queryKey: ['orgs', { key }],
      queryFn: getReposFn,
      // refetchOnMount: false,
      // refetchOnWindowFocus: false,
      // refetchOnReconnect: false,
      // refetchInterval: 1000,
      // staleTime: 1000 * 5,
      // cacheTime: 1000 * 60 * 5
    })

ここからは検証した結果のメモです。
試したこと

  1. React Router Dom を追加して2つの画面(A、B)を作る
  2. A画面でこれまでのようにリクエスト実行 & 表示した後に、画面をBに切り替える
  3. その後、元の画面 A に戻す

本質ではないですが、検証コードについても記載しておきます。

1. react-router-dom のインストール

yarn add react-router-dom

2. 画面遷移の定義を行い、main.ts はこれを指定する。

import { Link, Outlet, Route, Routes } from 'react-router-dom'
import App from './App'
import App2 from './App2'

export default function Root() {
  return (
    <div>
      <h1>Tanstack Query Cache Test</h1>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route path="app1" element={<App />} />
          <Route path="app2" element={<App2 />} />
        </Route>
      </Routes>
    </div>
  )
}

function Layout() {
  return (
    <div>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/app1">App1</Link>
          </li>
          <li>
            <Link to="/app2">App2</Link>
          </li>
        </ul>
      </nav>
      <Outlet />
    </div>
  )
}

3. 状態がわかりやすいように、App.tsx を変更

....
// HTMLに以下を追加
      <table border={1} style={{ borderCollapse: 'collapse' }}>
        <thead>
          <tr>
            <th>isInitialLoading</th>
            <th>isLoading</th>
            <th>isFetching</th>
            <th>isError</th>
            <th>data</th>
            <th>error</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>{String(isInitialLoading)}</td>
            <td>{String(isLoading)}</td>
            <td>{String(isFetching)}</td>
            <td>{String(isError)}</td>
            <td>{data && JSON.stringify(data).slice(0, 60)}...</td>
            <td>{String(error)}</td>
          </tr>
        </tbody>
      </table>

/app1 にアクセスすると、以下のように isLoading / isFetching が true になり、データ取得に行きます。

実行結果 - 取得中
実行結果 – 取得中

データ取得後は isLoadingisFetchingfalse になり、画面に表示されました。

実行結果 - 取得完了
実行結果 – 取得完了

ここまでが準備。ここから何パターンか確認してみます。

stale(古い)& cache 有効

最初の情報取得直後に stale になります。

実行結果(stale(古い)& cache 有効) - 取得直後
実行結果(stale(古い)& cache 有効) – 取得直後

その後、別画面に遷移後、再び戻ると、

・cache は有効期間中なので、画面に戻ったときには前のデータが表示されています。
・stale 状態なので再度リクエストが実行されます。取得後はデータに変更があれば更新されます。

以下は画面イメージです。最初 isFetching だけが true になります。データは出ていますね。

実行結果(stale(古い)& cache 有効) - 別画面に遷移後戻ってきた直後
実行結果(stale(古い)& cache 有効) – 別画面に遷移後戻ってきた直後

取得が終わると false になってます。

実行結果(stale(古い)& cache 有効) - 別画面に遷移後戻ってきてデータ取得が完了した後

実行結果(stale(古い)& cache 有効) – 別画面に遷移後戻ってきてデータ取得が完了した後

stale(古い)& cache 無効

今後は cacheTime を 3秒などに変更し、画面切り替え前に cache が無効になっているケースです。

最初の画面表示までは同じです。次に、別画面に遷移して戻ってきたときですが、

・cache が無効なので、データがありません。ので一覧には何も表示されていません。
・stale 状態であり、かつデータもないので、isFetching だえでなく isLoadingtrue になります。

実行結果(stale(古い)& cache 無効) - 別画面に遷移後戻ってきた直後
実行結果(stale(古い)& cache 無効) – 別画面に遷移後戻ってきた直後

データ取得後の表示はこれまでと同じになります(ので画面イメージは割愛)。

fresh(古くない)& cache 有効

今度は、stale ではない(Tanstack Query の devtool では fresh)場合です。
データ取得後は、画面は同じですが devtool 見てわかる通り、fresh になっています。

実行結果(fresh(古くない)& cache 有効) - データ取得後
実行結果(fresh(古くない)& cache 有効) – データ取得後

この状態で画面遷移 & 画面を戻すと以下となります。

・画面上には先程キャッシュしたデータが表示され、リクエストも投げられません。画面的には上と同じです。

うまくキャッシュが効いていますね。

fresh(古くない)& cache 無効

最後のパターンです。ここまで見るともうやるまでもないですが一応。

・この場合は「stale(古い)& cache 無効」の場合と同じになります。取得したデータの staleTime 期間だとしても、そもそも cache の有効期限を過ぎてしまうと、アクティブでないクエリのキャッシュは削除されます。ので、新規取得と同じフローに流れます(と理解しています)。

サマリ

4パターンのサマリは以下になります。

stalecache動き
true有効・遷移直後はキャッシュが使われ一覧表示
・サーバへリクエストを投げ、データ取得 & 更新する
true無効・遷移直後はキャッシュがないので一覧は空
・サーバへリクエストを投げ、データ取得 & 更新する
false有効・遷移直後はキャッシュが使われ一覧表示
・データは最新なのでサーバへリクエスト投げない
false無効・遷移直後はキャッシュがないので一覧は空
・データは最新なのでサーバへリクエスト投げない
検証パターン結果

まとめ

stale と cache が何を指しているかさえイメージが湧けば当然の結果ではありますが、改めて確認できた気がします。

ここで書いた設定たちはクエリ個別にも共通設定としても設定可能なので、柔軟に使えそうです。
ものによっては、特定タイミングでのみ更新したいと言ったこともあると思うので、そういうときはstaleTime を無限にするとずっと fresh にでき、更新したい場合はキャッシュを削除すると再取得にいってくれる、など。useQuery など Tanstack の便利なところは享受し、更新頻度とかは自分たちで制御するとかできそうです。