【React】React Query(TanStack Query)でデータ取得を効率化する

JavaScript

こんにちは、かつコーチです。

前回はfetchuseEffectを組み合わせた基本的なデータ取得の方法を解説しました。

isLoadingerrorの管理、リクエストのキャンセルまで自前で書くと、コード量がどんどん増えていくことを実感してもらえたかと思います。

今回はこの面倒な部分をまとめて解決してくれるライブラリ、React Query(TanStack Query)を紹介します。

上級者向けの内容になりますが、実務で使われる頻度が非常に高いライブラリなので、ぜひ押さえておきましょう。

なぜReact Queryが必要なのか

useEffect + fetchで自前実装する場合の課題

前回作ったような自前のuseFetchフックにも、実は見えていない課題がいくつも残っています。

  • 同じデータを複数のコンポーネントで使うとき、コンポーネントごとに再取得してしまう
  • 一度取得したデータをキャッシュして使い回す仕組みがない
  • 画面に戻ってきたときに自動でデータを最新化する仕組みがない
  • 通信失敗時のリトライ処理を毎回自分で書く必要がある

これらを全部自前で実装しようとすると、状態管理の複雑さがどんどん膨らんでいきます。

React Queryが解決してくれること

React Queryは「サーバーから取得したデータの状態管理」に特化したライブラリです。

キャッシュバックグラウンドでの再取得重複リクエストの排除リトライといった、サーバーデータ特有の面倒な処理を標準機能として提供してくれます。

ReduxやZustandがクライアント側の状態(UIの開閉状態など)を管理するのに対し、React Queryは「サーバー側のデータをどう同期させるか」に役割が特化している、と整理すると理解しやすいです。

基本の使い方

QueryClientProviderのセットアップ

React Queryを使うには、アプリ全体をQueryClientProviderで囲む必要があります。

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";

const queryClient = new QueryClient();

const Root = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  );
};

export default Root;

QueryClientは、取得したデータのキャッシュを一元管理する司令塔のような存在です。

アプリ内のどのコンポーネントからでも、このキャッシュを参照・更新できるようになります。

useQueryでデータ取得

実際のデータ取得はuseQueryフックを使います。

import { useQuery } from "@tanstack/react-query";

type User = {
  id: number;
  name: string;
};

const fetchUsers = async (): Promise<User[]> => {
  const res = await fetch("https://api.example.com/users");
  if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);
  return res.json();
};

const UserList = () => {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ["users"],
    queryFn: fetchUsers,
  });

  if (isLoading) return <p>読み込み中です...</p>;
  if (isError) return <p>エラーが発生しました: {(error as Error).message}</p>;

  return (
    <ul>
      {data?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

export default UserList;

前回自前で書いていたisLoadingerrorの管理はもちろん、キャッシュや再取得の仕組みまで、queryKeyqueryFnを渡すだけで手に入ります。

一度取得したデータはqueryKeyをもとにキャッシュされるため、別のコンポーネントで同じ["users"]を指定すると、再度通信せずキャッシュから即座に表示されます。

よくあるつまずきポイント:queryKeyの設計ミス

キャッシュが意図せず共有・分断される

React Queryを使い始めたばかりの頃、私はqueryKeyの設計でつまずきました。

❌ Before:パラメータをqueryKeyに含めない

const useUserDetail = (userId: number) => {
  return useQuery({
    queryKey: ["user"], // userIdが含まれていない
    queryFn: async () => {
      const res = await fetch(`https://api.example.com/users/${userId}`);
      return res.json();
    },
  });
};

このように書くと、userIdが1のときも2のときも同じqueryKey["user"])としてキャッシュされてしまいます。

その結果、別のユーザーの詳細ページに遷移しても、前のユーザーのキャッシュがそのまま表示されてしまうという事故が起きます。

私は実際にこのミスをやり、ユーザーAの詳細ページからユーザーBのページに移動したのに、一瞬ユーザーAの名前が表示されたままになる不具合を本番直前のレビューで指摘されたことがあります。

✅ After:可変のパラメータをqueryKeyに含める

const useUserDetail = (userId: number) => {
  return useQuery({
    queryKey: ["user", userId], // userIdごとに別キャッシュとして扱われる
    queryFn: async () => {
      const res = await fetch(`https://api.example.com/users/${userId}`);
      if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);
      return res.json();
    },
  });
};

queryKeyは「このデータが何によって変わるか」を表す識別子です。

取得するデータに影響するパラメータ(IDや検索条件など)は、必ずqueryKeyの配列に含めるようにしましょう。

応用・一歩先の使い方

useMutationでデータ更新とキャッシュの無効化

データの取得だけでなく、登録・更新・削除といった書き込み処理にはuseMutationを使います。

import { useMutation, useQueryClient } from "@tanstack/react-query";

type NewUser = {
  name: string;
  email: string;
};

const createUser = async (newUser: NewUser) => {
  const res = await fetch("https://api.example.com/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(newUser),
  });
  if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);
  return res.json();
};

const UserCreateForm = () => {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: createUser,
    onSuccess: () => {
      // usersのキャッシュを古いものとしてマークし、再取得させる
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });

  const handleSubmit = () => {
    mutation.mutate({ name: "新規ユーザー", email: "test@example.com" });
  };

  return (
    <button onClick={handleSubmit} disabled={mutation.isPending}>
      {mutation.isPending ? "登録中..." : "登録する"}
    </button>
  );
};

export default UserCreateForm;

onSuccessの中でinvalidateQueriesを呼ぶことで、「このキャッシュはもう古い」とReact Queryに伝え、一覧を自動的に再取得させています。

自分で「一覧のstateを直接書き換える」処理を書かなくても、キャッシュの整合性が保たれるのが大きなメリットです。

staleTimeとgcTimeの使い分け

React Queryにはキャッシュの鮮度を制御するstaleTimegcTimeという設定があります。

オプション役割
staleTimeデータを「新鮮」とみなす期間。この間は再取得しない
gcTime使われなくなったキャッシュをメモリから破棄するまでの時間

頻繁に変わらないマスタデータ(都道府県一覧など)はstaleTimeを長めに設定し、無駄な再取得を減らすのがおすすめです。

useQuery({
  queryKey: ["prefectures"],
  queryFn: fetchPrefectures,
  staleTime: 1000 * 60 * 60, // 1時間は再取得しない
});

逆に在庫数のようにリアルタイム性が求められるデータは、staleTimeを短く(または0に)しておくとよいでしょう。

まとめ

この記事のポイント

  • React Queryはサーバーデータ特有のキャッシュ・再取得・重複排除を標準機能として提供する
  • useQueryqueryKeyqueryFnを渡すだけでキャッシュ付きのデータ取得ができる
  • queryKeyにはデータに影響するパラメータ(IDなど)を必ず含める
  • 書き込み処理にはuseMutationを使い、成功時にinvalidateQueriesでキャッシュを無効化する
  • staleTimegcTimeを使い分けることで、無駄な再取得を減らせる

次に読むべき記事

→ 次の記事:React Hook Formでフォームバリデーションを実装する

タイトルとURLをコピーしました