TanStack Query 入門 — データフェッチの基本

概要

React でサーバーからデータを取得するとき、useEffect + useState で素朴に書くと、ローディング状態・エラー処理・キャッシュ・再取得を毎回手書きすることになります。これらは本質的にアプリ固有のロジックではなく、「非同期なサーバー状態をどう扱うか」という共通課題です。

TanStack Query(旧 React Query)は、このサーバー状態(server state)の管理に特化したライブラリです。本記事では入門として、基本的なデータフェッチの書き方と、押さえておきたい考え方を整理します。

環境

本記事のコード例は以下を想定しています。

  • React 18 以降
  • TypeScript 5 系
  • @tanstack/react-query v5
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 に置き換えてみると、恩恵が体感できるはずです。

\ 最新情報をチェック /

コメントを残す

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