TanStack Query v5 でサーバー状態を管理する
TanStack Query v5(旧 React Query)の最小構成とキャッシュの動き。status と fetchStatus の 2 軸、staleTime / gcTime の役割分担、同一 queryKey の重複排除、invalidateQueries の挙動、v4 からの破壊的変更(cacheTime 廃止・単一オブジェクト引数)を、動く demo と実測で確認する。
検証日: 2026-09-01
使用バージョン:
@tanstack/react-query@5.102.8(React 19)対象: fetch + useState でのデータ取得管理に限界を感じている / v4 の記事のコードが v5 で動かない場面
TanStack Query v5(旧 React Query)で サーバー状態(サーバーから取ってきたデータのローカルコピー)を管理する最小構成。キャッシュがいつ「新鮮」でいつ「再取得」されるかを、動く demo と実測ログで確認します。
触って試す
コンポーネント A / B は同じ queryKey を独立に useQuery している。それでも疑似 API の呼び出しは 1 回 — これが TanStack Query の中核で、「どのコンポーネントがデータを持つか」を設計しなくてよくなる。
なぜ TanStack Query か
useEffect + useState の自前 fetch 管理と比べて、次が最初から付いてくる:
- キャッシュと重複排除: 同じ
queryKeyの取得は 1 本にまとまる(demo の A / B) - stale-while-revalidate(古いデータを見せつつ裏で再取得): 再取得中も前のデータが表示され続ける
- リトライ・エラー状態・ローディング状態の一貫した表現
グローバル状態ライブラリ(Redux 等)の代替ではなく、「サーバー由来のデータ」専用のレイヤー。フォーム入力のようなクライアント状態には使わない。
最小サンプル(Provider + useQuery)
npm i @tanstack/react-query
| 項目 | 値 |
|---|---|
| 検証バージョン | @tanstack/react-query@5.102.8 |
| peer 要件 | React 18 / 19 |
| TypeScript 型 | 本体同梱 |
| v5 の要求環境 | ES2020(古いブラウザサポートは v4 まで) |
アプリのルートに QueryClientProvider を置き、コンポーネントで useQuery を呼ぶ:
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
const client = new QueryClient();
function App() {
return (
<QueryClientProvider client={client}>
<User />
</QueryClientProvider>
);
}
function User() {
const { data, isPending, isError, error } = useQuery({
queryKey: ["user", 1], // キャッシュの識別子(配列)
queryFn: () => fetch("/api/users/1").then((r) => r.json()),
});
if (isPending) return <p>読み込み中…</p>;
if (isError) return <p>エラー: {error.message}</p>;
return <p>{data.name}</p>;
}
status と fetchStatus は別の軸
v5 の状態は 2 軸で読む。status は「データがあるか」、fetchStatus は「いま通信しているか」。実測した遷移:
| タイミング | status | fetchStatus | isPending | isLoading |
|---|---|---|---|---|
| 初回 mount 直後 | pending | fetching | true | true |
| 初回成功後 | success | idle | false | false |
| refetch 中(データあり) | success | fetching | false | false |
isPending= データがまだ無い(status === "pending")isLoading=isPending && isFetching— つまり「初回だけの読み込み中」- refetch 中は
isLoadingにならない(前のデータを表示したまま裏で更新される)
「スピナーを出すのは初回だけ、更新中は今のデータを見せる」が、この 2 軸を使い分けるだけで書ける。
staleTime と gcTime(旧 cacheTime)
キャッシュの寿命を決める 2 つの時間は役割が違う:
| オプション | 意味 | 既定値 |
|---|---|---|
staleTime | この時間内は「新鮮」= マウント時・フォーカス時の自動再取得をしない | 0(即 stale) |
gcTime | 使っているコンポーネントが 1 つも無くなってから、キャッシュを捨てるまでの時間 | 5 分 |
gcTime の動きを query-core で実測したログ(gcTime を 100ms に設定):
unsubscribe 直後 cache: "data" ← アンマウント後もすぐには消えない
gcTime(100ms) 経過後 cache: undefined ← 経過後に GC される
invalidateQueries — 更新後の再取得
ミューテーション(データ更新)の後にキャッシュを更新する基本形が invalidateQueries。実測では staleTime: Infinity でも、アクティブな query は即座に再取得された:
const qc = useQueryClient();
await updateUser(form); // サーバーを更新して
qc.invalidateQueries({ queryKey: ["user"] }); // 関連キャッシュを無効化 → 自動再取得
queryKey は前方一致で効くので、["user"] を invalidate すると ["user", 1] / ["user", 2] もまとめて対象になる。
リトライの既定値はブラウザとサーバーで違う
失敗時の自動リトライは既定 3 回(計 4 回試行)— ただしこれはブラウザでの話。Node(SSR)で実行すると リトライ 0 回で即 error になる(実測: 試行 1 回で status: "error")。SSR とブラウザで「エラーの出やすさ」が変わるので、テストやサーバープリフェッチでは retry を明示するのが安全:
useQuery({ queryKey, queryFn, retry: 1 }); // 環境差を無くしたいとき
つまずいたポイント
- v4 の複数引数
useQuery(key, fn)が型エラー — v5 は単一オブジェクトのみ。機械的にuseQuery({ queryKey, queryFn })へ書き換える cacheTimeが黙って無視される — v5 ではgcTime。JS だと警告も出ないため、移行後にキャッシュ寿命が既定(5 分)へ戻っていても気づきにくいisLoadingの意味が v4 と違う — v4 のisLoadingは v5 のisPending。v5 のisLoadingは「初回取得中」に狭まった。ローディング表示の条件をisPendingにするかisLoadingにするかで、refetch 時の画面が変わる- リトライ回数が Node とブラウザで違う — 上記のとおり既定はブラウザ 3 / サーバー 0。vitest(Node 環境)でエラー系のテストを書くと「ブラウザより早く失敗する」ことに気づく
- demo は
client:only="react"で描画 —QueryClientをモジュールスコープに 1 つ作る構成のため、SSR ではリクエスト間でキャッシュを共有してしまう。SSR で使う場合は公式 docs の「Server Rendering」どおりリクエストごとにQueryClientを作る
関連書籍
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス | tech-book.net
Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScriptライブラリ「
実践Next.js -- App Routerで進化するWebアプリ開発 | tech-book.net
最新のNext.jsを現場で使うために実践的な知識を詰め込んだ一冊です。フレームワークの基礎はもちろん、パフォーマンスを上げるための知識や関連ライブラリまで、詳細なサン
関連記事 / 関連 Topic
- TanStack Table v8 を headless で使う — 同じ TanStack ファミリーのテーブル側
- Zod v4 のスキーマパターン —
queryFnのレスポンス検証に組み合わせると型が実行時保証になる - 体系的に学ぶなら: React の本 / TypeScript の本(tech-book.net)