こんにちは、かつコーチです。
前回はfetchとuseEffectを組み合わせた基本的なデータ取得の方法を解説しました。
isLoadingやerrorの管理、リクエストのキャンセルまで自前で書くと、コード量がどんどん増えていくことを実感してもらえたかと思います。
今回はこの面倒な部分をまとめて解決してくれるライブラリ、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;
前回自前で書いていたisLoading・errorの管理はもちろん、キャッシュや再取得の仕組みまで、queryKeyとqueryFnを渡すだけで手に入ります。
一度取得したデータは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にはキャッシュの鮮度を制御するstaleTimeとgcTimeという設定があります。
| オプション | 役割 |
|---|---|
staleTime | データを「新鮮」とみなす期間。この間は再取得しない |
gcTime | 使われなくなったキャッシュをメモリから破棄するまでの時間 |
頻繁に変わらないマスタデータ(都道府県一覧など)はstaleTimeを長めに設定し、無駄な再取得を減らすのがおすすめです。
useQuery({
queryKey: ["prefectures"],
queryFn: fetchPrefectures,
staleTime: 1000 * 60 * 60, // 1時間は再取得しない
});
逆に在庫数のようにリアルタイム性が求められるデータは、staleTimeを短く(または0に)しておくとよいでしょう。
まとめ
この記事のポイント
- React Queryはサーバーデータ特有のキャッシュ・再取得・重複排除を標準機能として提供する
useQueryはqueryKeyとqueryFnを渡すだけでキャッシュ付きのデータ取得ができるqueryKeyにはデータに影響するパラメータ(IDなど)を必ず含める- 書き込み処理には
useMutationを使い、成功時にinvalidateQueriesでキャッシュを無効化する staleTime・gcTimeを使い分けることで、無駄な再取得を減らせる
次に読むべき記事
→ 次の記事:React Hook Formでフォームバリデーションを実装する