TanStack Query 入門 — データフェッチの基本
概要
React でサーバーからデータを取得するとき、useEffect + useState で素朴に書くと、ローディング状態・エラー処理・キャッシュ・再取得を毎回手書きすることになります。これらは本質的にアプリ固有のロジックではなく、「非同期なサーバー状態をどう扱うか」という共通課題です。
TanStack Query(旧 React Query)は、このサーバー状態(server state)の管理に特化したライブラリです。本記事では入門として、基本的なデータフェッチの書き方と、押さえておきたい考え方を整理します。
環境
本記事のコード例は以下を想定しています。
- React 18 以降
- TypeScript 5 系
@tanstack/react-queryv5
npm install @tanstack/react-query
バージョンによって API 名が変わっている箇所があります(後述の「注意点」参照)。本記事は v5 を前提にしています。
サーバー状態とクライアント状態
TanStack Query を理解する最初のポイントは、状態を2種類に分けて考えることです。
| 種類 | 例 | 特徴 |
|---|---|---|
| クライアント状態 | モーダルの開閉、フォーム入力、タブ選択 | アプリが唯一の持ち主。即時に確定する |
| サーバー状態 | API から取得したユーザー一覧、商品データ | 持ち主はサーバー。手元のはコピー。古くなりうる |
useState や Redux などが得意なのは前者です。後者は「非同期」「他者が更新しうる」「キャッシュしたい」という性質があり、専用の道具があると圧倒的に楽になります。TanStack Query はまさに後者のための道具です。
まずは Provider を設定する
アプリのルート付近で QueryClient を生成し、QueryClientProvider でラップします。
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Users />
</QueryClientProvider>
);
}
QueryClient がキャッシュの実体を持ちます。アプリ内で基本的に1つ用意すれば十分です。
useQuery でデータを取得する
データ取得の基本は useQuery です。
import { useQuery } from "@tanstack/react-query";
type User = {
id: number;
name: string;
};
async function fetchUsers(): Promise<User[]> {
const res = await fetch("https://example.com/api/users");
if (!res.ok) {
throw new Error("ユーザーの取得に失敗しました");
}
return res.json();
}
function Users() {
const { data, isPending, isError, error } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
});
if (isPending) return <p>読み込み中…</p>;
if (isError) return <p>エラー: {error.message}</p>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
useEffect も useState も出てきません。ローディング(isPending)・エラー(isError)・成功(data)が最初から用意され、分岐するだけで済みます。
ここで重要なのが2つの引数です。
queryKey— このデータを識別するキー。配列で指定する。キャッシュのキーでありつつ、依存配列の役割も持つqueryFn— 実際にデータを取る非同期関数。Promise を返し、失敗時は throw する
queryFn は「成功なら resolve、失敗なら throw」という素直な契約です。fetch は 404 や 500 でも reject しないため、res.ok を見て明示的に throw する点に注意してください。
queryKey の設計
queryKey はキャッシュの住所です。パラメータに応じてキーを分けることで、別々のデータとしてキャッシュされます。
function UserDetail({ userId }: { userId: number }) {
const { data } = useQuery({
queryKey: ["users", userId],
queryFn: () => fetchUser(userId),
});
// ...
}
["users", 1] と ["users", 2] は別のキャッシュエントリになります。userId が変わると自動的に再取得されるため、依存配列を手で管理する必要がありません。
キーの階層は、後述のキャッシュ無効化でまとめて扱うときにも効いてきます(["users"] を指定すると ["users", 1] なども対象にできる)。
なぜ嬉しいのか — デフォルトの振る舞い
TanStack Query はただデータを取るだけではありません。デフォルトで次のような振る舞いを持ちます。
- キャッシュ: 同じ
queryKeyのデータは共有される。別コンポーネントで同じクエリを使っても、取得は1回で済む - バックグラウンド再取得: ウィンドウにフォーカスが戻ったとき、ネットワーク再接続時などに自動で最新化
- 重複排除(dedupe): 同時に同じクエリが走っても、リクエストは1本にまとめられる
- リトライ: 失敗時に自動で数回リトライ
これらを useEffect で手書きすると相応のコード量になります。「サーバー状態の面倒ごと」をライブラリ側に寄せられるのが最大の価値です。
staleTime と gcTime
挙動を理解するうえで外せないのが、データの「鮮度」を決める2つの時間設定です。
useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
staleTime: 1000 * 60, // 1分間は "新鮮" とみなす
gcTime: 1000 * 60 * 5, // 未使用になって5分でキャッシュ破棄
});
staleTime— データを「新鮮(fresh)」とみなす時間。この間はフォーカス復帰などで再取得が走らない。デフォルトは0(取得直後からすぐ stale 扱い)gcTime— そのクエリを使うコンポーネントが無くなってから、キャッシュをメモリに保持する時間(旧名cacheTime)
入門段階でまず効果を実感しやすいのは staleTime です。デフォルトの 0 だと「フォーカスを戻すたびに再取得」が気になる場合があるので、更新頻度が低いデータには適切な staleTime を設定すると無駄な通信を減らせます。
データの更新 — useMutation
取得(query)と更新(mutation)は分かれています。作成・更新・削除には useMutation を使います。
import { useMutation, useQueryClient } from "@tanstack/react-query";
function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (name: string) =>
fetch("https://example.com/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
}).then((res) => {
if (!res.ok) throw new Error("作成に失敗しました");
return res.json();
}),
onSuccess: () => {
// 一覧のキャッシュを無効化 → 自動で再取得される
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
return (
<button
disabled={mutation.isPending}
onClick={() => mutation.mutate("新しいユーザー")}
>
追加
</button>
);
}
更新のポイントは onSuccess の中の invalidateQueries です。「更新したら、関係するクエリを無効化して取り直す」という流れが定石です。手元のキャッシュを自前で書き換えるより、サーバーを正とする素直な設計になります。
確認方法
導入後に挙動を確認するには、公式の DevTools が便利です。
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// Provider の中に置く
<ReactQueryDevtools initialIsOpen={false} />
各クエリのキー・状態(fresh / stale / fetching)・データ内容が可視化されます。「なぜ再取得されたのか」「キャッシュに乗っているか」を目で追えるので、入門時こそ入れておくと理解が早まります。
注意点
- v4→v5 の名称変更:
isLoadingは多くのケースでisPendingに、cacheTimeはgcTimeに変わりました。古い記事のコードがそのまま動かないことがあります staleTime=0がデフォルト: 「フォーカスするたび再取得する」のは不具合ではなく既定動作です。気になる場合はstaleTimeを設定する- クライアント状態まで載せない: モーダルの開閉やフォーム入力まで TanStack Query で管理しようとしない。あくまでサーバー状態のための道具です
queryFnは throw で失敗を表す:fetchのres.okチェックを忘れると、エラーがエラーとして扱われない
まとめ
- TanStack Query は「サーバー状態」を専門に扱うライブラリ
- 取得は
useQuery(queryKey+queryFn)、更新はuseMutation - キャッシュ・再取得・リトライ・重複排除がデフォルトで効く
- 鮮度は
staleTime/gcTimeで制御する - 更新後は
invalidateQueriesで無効化し、サーバーを正とする
useEffect での素朴なデータフェッチに疲れたら、まず一覧画面ひとつを useQuery に置き換えてみると、恩恵が体感できるはずです。
