React開発者必見!TanStack Queryで実現する快適なデータ取得

はい、承知いたしました。React開発者向けに、TanStack Query(旧React Query)を用いた快適なデータ取得について、約5000語の詳細な解説記事を作成します。


React開発者必見!TanStack Queryで実現する快適なデータ取得

はじめに:データ取得の苦痛からの解放

Reactアプリケーションを開発している皆さん、日々のデータ取得にどんな苦労を感じていますか?

  • useEffect フックの中で非同期処理を書き、ローディング状態、エラー状態、データ本体の3つの状態をコンポーネントのステートで管理する boilerplate コードにうんざりしていませんか?
  • 取得したデータを他のコンポーネントと共有するために、Props Drilling をしたり、Context API や Redux のような状態管理ライブラリに詰め込んだりすることに複雑さを感じていませんか?
  • 一度取得したデータをキャッシュし、不要なネットワークリクエストを減らしたいのに、そのキャッシュ管理が自作だとすぐに破綻してしまう経験はありませんか?
  • コンポーネントのマウント/アンマウントによるデータフェッチの競合状態 (Race Condition) に悩まされたり、古いデータが表示されてしまう問題に直面したりしたことはありませんか?
  • バックグラウンドでのデータ更新、フォーカス時のリフレッシュ、オフライン時の挙動など、細かい UX の向上施策を実装するのが面倒だと感じていませんか?

もし一つでも「はい」と答えたなら、それはまさにサーバー状態 (Server State) の管理に苦労している証拠です。React のコンポーネントステートや一般的な状態管理ライブラリは、UI の状態(例えばモーダルの開閉やフォームの入力値)を管理するのに長けていますが、サーバー状態、つまりリモートの非同期ソース(API エンドポイントなど)から取得されるデータの管理には、いくつかの根本的な課題があります。

サーバー状態は、クライアントの状態とは異なる特性を持っています。

  1. 非同期性: 取得には時間がかかります。
  2. 所有権: クライアントではなく、リモートソース(サーバー)がデータの真の所有者です。
  3. 共有性: 複数のクライアント(アプリケーションの異なるコンポーネント)が同じデータを参照する可能性があります。
  4. 陳腐化 (Staleness): データは時間と共に古くなる可能性があります。
  5. 永続性: アプリケーションのライフサイクルを超えて存在します(キャッシュが必要)。

これらの特性をクライアントの状態管理のパラダイムで扱うのは、非常に骨が折れる作業です。多くの boilerplate が生まれ、バグの原因となり、開発体験を著しく低下させます。

ここで登場するのが、TanStack Query (旧 React Query) です。TanStack Query は、React アプリケーションにおけるサーバー状態の管理を専門とする強力なライブラリです。データ取得、キャッシング、同期、バックグラウンドでの更新、エラーハンドリング、ローディング状態の管理など、サーバー状態に関連するあらゆる面倒なタスクを抽象化し、開発者が本来注力すべき UI の開発に集中できるように設計されています。

TanStack Query を導入することで、先ほど挙げたようなデータ取得に関する苦痛から解放され、より快適で堅牢なアプリケーション開発が可能になります。この記事では、TanStack Query がどのようにこれらの課題を解決するのか、その主要な概念、基本的な使い方から高度な機能、実践的なテクニックまでを、詳細なコード例を交えながら徹底的に解説します。

さあ、TanStack Query の世界に飛び込み、快適なデータ取得を体験しましょう!

なぜ TanStack Query なのか? 従来のデータ取得手法との決定的な違い

TanStack Query の前に、なぜ従来の useEffect や他の状態管理手法だけではサーバー状態の管理が難しいのかをもう少し深く掘り下げてみましょう。

従来の useEffect を使ったデータ取得の課題:

  1. Boilerplate コードの多さ: データをフェッチするたびに、ローディング (isLoading)、エラー (isErrorerror)、データ本体 (data) の3つのステートを useState で定義し、useEffect の中で非同期関数を呼び出し、それぞれのステートを適切に更新する必要があります。さらに、クリーンアップ関数でフェッチ中の Promise をキャンセルしないと、コンポーネントがアンマウントされた後にステートを更新しようとしてメモリリークやエラーの原因になります。これは同じパターンがアプリケーションのあちこちに散らばることを意味し、保守性を低下させます。
  2. 手動でのキャッシュ管理: データをキャッシュしたい場合、自分で Map オブジェクトなどを使ってインメモリキャッシュを実装するか、より高機能なライブラリを組み合わせる必要があります。キャッシュの有効期限、無効化、ガベージコレクションなどを適切に管理するのは非常に複雑です。データが更新されたら関連するキャッシュを無効化し、再フェッチをトリガーする必要がありますが、これも手動では抜け漏れが発生しがちです。
  3. 陳腐化したデータの表示: キャッシュがある場合、ユーザーに最新データを見せるためには定期的な更新が必要です。しかし、どれくらいの頻度で更新すべきか、ユーザーが他のタブに切り替えている間は更新を一時停止するかなど、細かな制御が必要になります。手動実装では、これらの UX を考慮したバックグラウンド更新は非常に骨が折れます。
  4. 競合状態 (Race Condition): useEffect の依存配列が変更された際に、前回の非同期処理が完了する前に新しい非同期処理が開始されると、完了順序が保証されません。遅れて完了した古いリクエストの結果でステートが上書きされてしまい、誤ったデータが表示される可能性があります。クリーンアップ関数である程度防げますが、完璧ではありません。
  5. データの共有と同期: 複数のコンポーネントが同じデータを必要とする場合、それぞれのコンポーネントで個別にフェッチするか、共通の親でフェッチして Props で渡すか、Context や Redux に格納する必要があります。個別のフェッチは非効率ですし、Props Drilling や状態管理ライブラリへの格納はコンポーネントツリーの複雑さを増します。また、一箇所でデータを更新した場合に、そのデータを参照している他のすべての箇所で最新データが反映されるように手動で同期させる必要があります。
  6. ユーザー体験の向上: ローディング中のスピナー表示、エラーメッセージ表示、リトライ処理、ページネーションや無限スクロールの実装、フォーカス時の自動リフレッシュなど、快適なユーザー体験を提供するための機能実装は、すべて手動で行うと大きな工数とバグのリスクを伴います。

TanStack Query がこれらの課題をどう解決するか:

TanStack Query は、サーバー状態の管理に特化したフック群を提供することで、これらの問題を鮮やかに解決します。

  1. Boilerplate の削減: useQueryuseMutation といった専用のフックを使えば、ローディング、エラー、データといった状態がフックの戻り値として提供されます。開発者はステートの定義や useEffect の非同期処理、クリーンアップ関数といった boilerplate から解放され、より宣言的なコードを書くことができます。
  2. 賢い自動キャッシュ: TanStack Query の最も強力な機能の一つが、自動的かつインテリジェントなキャッシュ管理です。データをフェッチすると、そのデータは Query Client の内部キャッシュに格納されます。同じデータを要求する別のコンポーネントは、ネットワークリクエストを行わずにキャッシュから瞬時にデータを取得できます。キャッシュの有効期限 (staleTime, cacheTime) や無効化も、TanStack Query が面倒を見てくれます。
  3. stale-while-revalidate モデル: TanStack Query はキャッシュされたデータを「陳腐 (stale)」な状態と「新鮮 (fresh)」な状態に区別します。デフォルトでは、データは取得後すぐに fresh になりますが、一定時間(デフォルト0秒)経過すると stale になります。コンポーネントがマウントされた際にデータが stale であれば、TanStack Query はバックグラウンドで新しいデータをフェッチしつつ、すぐにキャッシュにある古いデータを表示します。新しいデータの取得が完了したら、UI が最新データにシームレスに切り替わります。これは HTTP キャッシュの stale-while-revalidate 戦略と同様で、ユーザーはデータが表示されるのを待つ必要がなく、常に高速な初期表示と最新データの両立を実現できます。
  4. バックグラウンドでのデータ更新: ウィンドウにフォーカスが戻ったとき、アプリケーションがオンラインに復帰したとき、または特定のイベントが発生したときなど、様々なタイミングでバックグラウンドでのデータ更新を自動的に行うことができます。これにより、複数のユーザーが同じデータを参照している場合でも、各クライアントは常に最新の状態に近く保たれます。
  5. 集中管理されたサーバー状態: TanStack Query の Query Client は、アプリケーション全体のサーバー状態を一元管理します。これにより、複数のコンポーネントが同じ Query Key を指定して useQuery を呼び出すと、内部的には一度だけネットワークリクエストが行われ、結果がキャッシュされ、そのキャッシュがすべての購読コンポーネントに配信されます。データの更新(Mutation)を行った際も、関連する Query を無効化 (invalidate) するだけで、その Query を使っているすべての箇所が自動的に最新データに更新されます。データの共有と同期が非常に容易になります。
  6. 高度な機能の提供: リトライ、ポーリング、ページネーション、無限スクロール、楽観的更新 (Optimistic Updates) といった高度なデータ取得・更新パターンを、比較的少ないコードで実現するためのフックやオプションが用意されています。
  7. 開発者ツールの提供: TanStack Query には優れた開発者ツールが付属しており、Query の状態、キャッシュの内容、Mutation の状況などを視覚的に確認できます。これにより、データフローの問題のデバッグが劇的に容易になります。

要するに、TanStack Query はサーバー状態管理の複雑さをブラックボックス化し、開発者にはシンプルで強力な API (フック) を提供します。これはまさに、React が DOM 操作を抽象化して宣言的な UI 構築を可能にしたのと同様のパラダイムシフトを、データ取得・管理にもたらすものです。

TanStack Query の核となる概念

TanStack Query を使い始める前に、その主要な概念を理解しておくことが重要です。

  1. Query (クエリ)

    • サーバーからデータを「読み取る」操作を表します。
    • 例えば、「ユーザーリストを取得する」「特定の商品情報を取得する」といった操作です。
    • useQuery フックを使って定義します。
    • Query は一意の Query Key によって識別されます。
    • Query はキャッシュされ、そのライフサイクル (fetching, fresh, stale, inactive) が管理されます。
  2. Mutation (ミューテーション)

    • サーバー上のデータを「変更する」操作を表します。
    • 例えば、「新しいユーザーを作成する」「商品を更新する」「投稿を削除する」といった操作です。
    • useMutation フックを使って定義します。
    • Mutation はデータを変更するため、関連する Query のキャッシュを陳腐化 (invalidate) させ、再フェッチを促すことが一般的です。
    • 楽観的更新 (Optimistic Updates) を実装するための強力な機能を提供します。
  3. Query Client (クエリクライアント)

    • TanStack Query の中核となるオブジェクトです。
    • Query のキャッシュ、Mutation のキュー、および Query と Mutation のインタラクションを管理します。
    • 通常、アプリケーションのルートコンポーネントの近くで一度だけ作成し、QueryClientProvider を使ってコンポーネントツリー全体に提供します。
  4. Query Key (クエリキー)

    • 各 Query を一意に識別するためのキーです。
    • 非常に重要な概念です。TanStack Query はこのキーを使ってキャッシュを管理します。
    • 配列で表現されることが推奨されます。最初の要素は取得するデータの種類を表す文字列、続く要素はデータの特定に必要なパラメータ(ID、フィルター条件など)を含めます。
    • 例:
      • ユーザーリスト: ['users']
      • IDが1のユーザー: ['user', 1]
      • 特定の状態のtodos: ['todos', { status: 'completed' }]
      • ページ番号付きの商品リスト: ['products', { page: 2 }]
    • Query Key が変更されると、TanStack Query は新しいデータとして扱い、再フェッチを試みます(ただし、キャッシュが存在し、かつ stale でない場合はキャッシュを返します)。
  5. Query Client Provider (クエリクライアントプロバイダー)

    • 作成した QueryClient インスタンスを、アプリケーションの React コンポーネントツリーに提供するためのコンポーネントです。
    • TanStack Query のフック (useQuery, useMutationなど) は、この Provider が提供するクライアントインスタンスを通じて内部的に Query Client とやり取りします。

TanStack Query を使ってみる:基本的なセットアップと useQuery

それでは、実際に TanStack Query を使ってデータを取得する手順を見ていきましょう。

1. インストール

まずはライブラリをインストールします。React アプリケーションであれば @tanstack/react-query を使います。

“`bash
npm install @tanstack/react-query

または

yarn add @tanstack/react-query

または

pnpm add @tanstack/react-query
“`

開発者ツールも同時にインストールしておくと便利です。

“`bash
npm install @tanstack/react-query-devtools -D

または

yarn add @tanstack/react-query-devtools -D

または

pnpm add @tanstack/react-query-devtools -D
“`

2. セットアップ

アプリケーションのルートレベルで QueryClient を作成し、QueryClientProvider でアプリケーションをラップします。

例: src/App.tsx または src/index.tsx

“`jsx
// src/index.tsx または src/App.tsx

import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import App from ‘./App’;
import {
QueryClient,
QueryClientProvider,
} from ‘@tanstack/react-query’;
import { ReactQueryDevtools } from ‘@tanstack/react-query-devtools’; // 開発者ツール

// Query Client インスタンスを作成
const queryClient = new QueryClient({
// デフォルトのクエリオプションを設定することも可能
defaultOptions: {
queries: {
// 例えば、エラー時に自動でリトライしないように設定
// retry: false,
// データを fresh に保つ時間を設定 (デフォルト0ms)
// staleTime: 1000 * 60 * 5, // 5分間 fresh
},
},
});

const root = ReactDOM.createRoot(
document.getElementById(‘root’) as HTMLElement
);

root.render(

{/ QueryClientProvider でアプリケーションをラップ /}


{/ 開発者ツールを追加 (本番環境では表示しないように条件分岐することが多い) /}



);
“`

これで、アプリケーションのどこからでも TanStack Query のフックを使用する準備が整いました。

3. useQuery を使った基本的なデータ取得

useQuery フックを使ってデータを取得します。useQuery は少なくとも2つの引数を取ります。

  1. queryKey: Query を一意に識別するためのキー(配列)。
  2. queryFn: データをフェッチするための Promise を返す関数。

useQuery フックは、Query の状態に関する様々な情報を持つオブジェクトを返します。主に以下のプロパティを使用します。

  • data: フェッチされたデータ。ローディング中やエラー時は undefined
  • isLoading: データが初めてフェッチされている最中か (Query Key がまだキャッシュに存在しない場合)。
  • isFetching: Query がアクティブにフェッチされている最中か (バックグラウンドでのリフレッシュなども含む)。isLoading の上位互換のようなものです。
  • isError: フェッチ中にエラーが発生したか。
  • error: エラーが発生した場合のエラーオブジェクト。
  • status: Query の状態を表す文字列 ('loading', 'error', 'success'). 'loading'isLoading === true と同等ですが、'success' の場合でもバックグラウンドでフェッチ中 (isFetching === true) という状態がありえます。status だけでは isFetching かどうかは分かりません。一般的には isLoadingisError を使って条件分岐することが多いです。

例: ユーザーリストを取得するコンポーネント

“`jsx
// src/components/UserList.tsx

import React from ‘react’;
import { useQuery } from ‘@tanstack/react-query’;

// データをフェッチする非同期関数 (APIコールを想定)
// 例として JSONPlaceholder の users エンドポイントを使用
const fetchUsers = async () => {
const res = await fetch(‘https://jsonplaceholder.typicode.com/users’);
if (!res.ok) {
throw new Error(‘Failed to fetch users’);
}
return res.json();
};

function UserList() {
const {
data: users, // 取得したデータ。data という名前を users に変更
isLoading, // 初回ローディング中か
isFetching, // バックグラウンドを含むフェッチ中か
isError, // エラーが発生したか
error, // エラーオブジェクト
status, // ‘loading’, ‘error’, ‘success’
} = useQuery({
queryKey: [‘users’], // Query キーは一意の配列
queryFn: fetchUsers, // データを取得する関数
});

// ステータスに基づいた条件分岐
if (isLoading) {
return

Loading users…

;
}

if (isError) {
// エラーオブジェクトは error プロパティで取得できる
return

Error fetching users: {(error as Error).message}

;
}

// データ取得成功時
return (

User List

{isFetching && !isLoading && (
// 初回ローディング中でなく、バックグラウンドでフェッチ中の場合
(Updating in background…)
)}

    {users?.map((user: any) => ( // データの型は適切に定義することが推奨

  • {user.name}
  • ))}

);
}

export default UserList;
“`

このコンポーネントをアプリケーションに追加して表示すると、ユーザーリストがフェッチされ、表示されるのが確認できます。

キャッシュの確認 (Devtools)

ここで開発者ツールを開いてみましょう。ブラウザに TanStack Query の開発者ツールウィンドウが表示されているはずです(デフォルトでは画面右下にアイコンが表示されます)。

開発者ツールを見ると、['users'] という Query が追加され、その状態が確認できます。おそらく最初は loading になり、データ取得が成功すると success (または fresh) の状態になるはずです。

同じ UserList コンポーネントを別の場所にもう一つ配置してみるとどうなるでしょうか?

“`jsx
// src/App.tsx
import UserList from ‘./components/UserList’;

function App() {
return (


{/ もう一つ同じコンポーネントを追加 /}

);
}
“`

ブラウザをリロードして見てください。ネットワークタブを見ると、jsonplaceholder.typicode.com/users へのリクエストは一度しか行われないことがわかります。これは、一つ目の UserList がデータをフェッチしてキャッシュに格納し、二つ目の UserList はマウントされた際にキャッシュに ['users'] のデータが存在することを知り、すぐにキャッシュからデータを取得して表示するからです。これが TanStack Query の自動キャッシュの力です。

開発者ツールで見ると、['users'] の Query が active な状態になっていることが確認できます。これは、その Query を使用しているコンポーネント(インスタンス)が現在マウントされていることを示します。

TanStack Query の高度な Query 機能

基本的なデータ取得ができるようになったところで、TanStack Query の強力な機能をいくつか見ていきましょう。

1. Query オプションの活用

useQuery は第3引数(または単一のオプションオブジェクト)で様々な設定が可能です。これらを活用することで、Query の振る舞いを細かく制御できます。

よく使うオプション:

  • staleTime: キャッシュされたデータが「新鮮 (fresh)」であると見なされる時間(ミリ秒)。この時間を過ぎるとデータは「陳腐 (stale)」になります。デフォルトは 0 なので、データは取得後すぐに stale になります。staleTime を設定すると、その時間はキャッシュが使われ、バックグラウンドでの再フェッチは行われません(ただし、手動での refetch や invalidate は別)。
    • 例: staleTime: 1000 * 60 * 5 (5分間 fresh)
  • cacheTime: 非アクティブな Query (つまり、その Query を使用しているコンポーネントが全てアンマウントされた状態) が、キャッシュ内で保持される時間(ミリ秒)。この時間を過ぎると、Query はガベージコレクションされ、キャッシュから削除されます。デフォルトは 1000 * 60 * 5 (5分)。
    • 例: cacheTime: 1000 * 60 * 30 (30分保持)
  • enabled: Query を自動的に実行するかどうかを制御します。デフォルトは truefalse にすると、手動で refetch を呼び出すまで Query は実行されません。特定の条件が満たされた場合にのみフェッチを開始したい場合などに便利です。
    • 例: enabled: !!userId (userId が存在する場合のみフェッチ)
  • retry: Query が失敗した場合に自動的にリトライする回数。デフォルトは 3 回。false にするとリトライしません。
    • 例: retry: 5 (5回リトライ), retry: false (リトライしない)
  • retryDelay: リトライ間の待機時間を設定する関数またはミリ秒。指数関数的バックオフなどを実装できます。
  • refetchOnWindowFocus: ウィンドウにフォーカスが戻った際に Query を自動的に再フェッチするかどうか。デフォルトは true。ユーザーに最新データを見せるためによく使われる機能です。
  • refetchOnMount: コンポーネントがマウントされた際に Query を自動的に再フェッチするかどうか。デフォルトは true (ただし、データが fresh な場合はフェッチしない)。
  • refetchOnReconnect: ネットワークがオンラインに復帰した際に Query を自動的に再フェッチするかどうか。デフォルトは true
  • select: フェッチされたデータの一部を抽出したり、変換したりするための関数。コンポーネントが必要とするデータの形に整形するために使います。これにより、コンポーネントがキャッシュ全体のデータ構造に依存するのを避けたり、データの一部だけが必要な場合に不必要なレンダリングを防いだりできます。

例: オプションを使った Query

“`jsx
import { useQuery } from ‘@tanstack/react-query’;

const fetchPost = async (postId: number) => {
const res = await fetch(https://jsonplaceholder.typicode.com/posts/${postId});
if (!res.ok) throw new Error(‘Failed to fetch post’);
return res.json();
};

function PostDetail({ postId }: { postId: number }) {
const { data: post, isLoading, isError, error } = useQuery({
queryKey: [‘post’, postId], // ID をキーに含める
queryFn: () => fetchPost(postId),
enabled: !!postId, // postId がある場合のみフェッチ
staleTime: 1000 * 60 * 10, // 10分間 fresh
retry: false, // リトライしない
select: (data) => ({ // select を使って必要なデータだけを抽出・整形
id: data.id,
title: data.title,
bodyPreview: data.body.substring(0, 100) + ‘…’,
}),
});

if (isLoading) return

Loading post…

;
if (isError) return

Error: {(error as Error).message}

;

return (

{post.title}

{post.bodyPreview}

{/ フルボディデータはキャッシュされているが、コンポーネントは select で整形されたデータのみを使用 /}

);
}
“`

2. Query Invalidation (クエリの無効化)

データがサーバー側で変更されたことをアプリケーションが知ったとき(例えば Mutation の成功後)、そのデータに関連する Query のキャッシュを「陳腐化 (invalidate)」させ、必要に応じてバックグラウンドで再フェッチさせたい場合があります。queryClient.invalidateQueries メソッドを使用します。

invalidateQueries は Query Key または Query Key の一部を受け取り、マッチするすべての Query を stale 状態にします。もしその Query が現在アクティブであれば、自動的にバックグラウンドで再フェッチがトリガーされます。

例: 新しい todo を追加した後に todo リストを更新する

“`jsx
import { useMutation, useQueryClient } from ‘@tanstack/react-query’;

const addTodo = async (newTodo: { title: string }) => {
const res = await fetch(‘https://jsonplaceholder.typicode.com/todos’, {
method: ‘POST’,
body: JSON.stringify(newTodo),
headers: { ‘Content-Type’: ‘application/json’ },
});
if (!res.ok) throw new Error(‘Failed to add todo’);
return res.json();
};

function TodoForm() {
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: addTodo,
onSuccess: () => {
// todo の追加に成功したら、’todos’ キーを持つ Query を無効化
queryClient.invalidateQueries({ queryKey: [‘todos’] });
console.log(‘Todo list invalidated and will refetch’);
},
});

const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const title = formData.get(‘title’) as string;
mutation.mutate({ title }); // addTodo を実行
};

return (



{mutation.isError &&

Error adding todo: {(mutation.error as Error).message}

}

);
}
“`

この例では、useMutation で todo の追加を行い、成功時に queryClient.invalidateQueries({ queryKey: ['todos'] }) を呼び出しています。アプリケーション内のどこかで ['todos'] という Query Key を使って Todo リストを表示しているコンポーネントがあれば、その Query は stale とマークされ、もしアクティブならば自動的に再フェッチが実行されます。

invalidateQueries は部分的なキーマッチングもサポートしています。例えば、queryClient.invalidateQueries({ queryKey: ['todos', todoId] }) のように特定の todo だけを無効化したり、queryClient.invalidateQueries({ queryKey: ['todos'] }) のように 'todos' で始まる全ての Query を無効化したりできます。

3. Pagination / Infinite Queries (ページネーション / 無限スクロール)

リスト表示でよく使われるページネーションや無限スクロールも、TanStack Query は強力にサポートします。

a) ページネーション (useQuerykeepPreviousData)

ページ番号やオフセットなどのパラメータを Query Key に含めることで、ページごとに異なる Query として扱うことができます。しかし、ページ遷移時に新しいデータが取得されるまで前のページの内容が消えてしまうと、ユーザー体験が損なわれます。keepPreviousData: true オプションを使うと、新しいデータをフェッチしている間も前のページのデータを表示し続けることができます。新しいデータの取得が完了すると、データはシームレスに切り替わります。

“`jsx
import { useQuery } from ‘@tanstack/react-query’;
import React, { useState } from ‘react’;

const fetchItems = async (page: number) => {
const res = await fetch(https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10);
if (!res.ok) throw new Error(‘Failed to fetch items’);
return res.json();
};

function PaginatedItems() {
const [page, setPage] = useState(1);

const {
data: items,
isLoading,
isError,
error,
isFetching, // 新しいページをフェッチ中かどうかがわかる
isPreviousData, // 前のページデータを表示中か
} = useQuery({
queryKey: [‘items’, page], // ページ番号をキーに含める
queryFn: () => fetchItems(page),
keepPreviousData: true, // ★ これ重要!新しいページをフェッチ中も前のページデータを表示
staleTime: 1000 * 60, // 1分間 fresh
});

if (isLoading) return

Loading items…

;
if (isError) return

Error: {(error as Error).message}

;

return (

Items (Page {page})

{isFetching && isPreviousData ? (
// 前のページを表示しつつ、新しいページをバックグラウンドでフェッチ中

Updating…

) : null}

    {items?.map((item: any) => (

  • {item.title}
  • ))}


);
}
“`

keepPreviousData: true を使うことで、ページ遷移時のローディング状態でもコンテンツエリアが空白になることを防ぎ、スムーズな切り替えを実現できます。isFetching を見て、新しいデータをフェッチ中であることを示す UI を表示するのも良いプラクティスです。

b) 無限スクロール (useInfiniteQuery)

「もっと見る」ボタンやスクロールダウンで次のページのデータをロードして、現在のリストに追加していく無限スクロールには、useInfiniteQuery フックを使用します。

useInfiniteQueryuseQuery と似ていますが、以下の点が異なります。

  • 返される data の構造が { pages: [], pageParams: [] } のようになります。pages 配列に各ページで取得したデータが格納されます。
  • queryFn{ pageParam } を引数として受け取ります。これは次のページのデータを取得するために使用するパラメータです。
  • getNextPageParam または getPreviousPageParam オプションが必要です。これは、最後のページのデータを受け取り、次のページをフェッチするための pageParam を計算して返す関数です。これらが undefined を返すと、それ以上ページがないと判断されます。
  • fetchNextPage / fetchPreviousPage 関数が返されます。これを呼び出すことで、次の/前のページのデータをフェッチできます。
  • hasNextPage / hasPreviousPage が返されます。次の/前のページが存在するかどうかを示します(getNextPageParam / getPreviousPageParam の戻り値に基づきます)。
  • isFetchingNextPage / isFetchingPreviousPage が返されます。次の/前のページをフェッチ中かどうかを示します。

“`jsx
import { useInfiniteQuery } from ‘@tanstack/react-query’;
import React from ‘react’;

const fetchInfiniteItems = async ({ pageParam = 1 }) => { // pageParam をデフォルト1で受け取る
const res = await fetch(https://jsonplaceholder.typicode.com/posts?_page=${pageParam}&_limit=10);
if (!res.ok) throw new Error(‘Failed to fetch items’);
return res.json();
};

function InfiniteItems() {
const {
data, // 取得したページデータの構造: { pages: [[…page1], […page2]], pageParams: […] }
fetchNextPage, // 次のページをフェッチする関数
hasNextPage, // 次のページが存在するかどうか
isFetchingNextPage, // 次のページをフェッチ中か
isLoading,
isError,
error,
} = useInfiniteQuery({
queryKey: [‘infiniteItems’], // 全体の Query キー
queryFn: fetchInfiniteItems,
getNextPageParam: (lastPage, allPages) => {
// lastPage: 最後に取得したページのデータ
// allPages: これまでに取得した全てのページの配列
// 次のページ番号を計算。データが空なら終わりとする。
if (lastPage.length === 0) return undefined;
return allPages.length + 1;
},
// getPreviousPageParam も同様に定義可能
});

if (isLoading) return

Loading items…

;
if (isError) return

Error: {(error as Error).message}

;

// 全ページのアイテムをフラットな配列に変換
const items = data?.pages.flatMap(page => page) || [];

return (

Infinite Items

    {items.map((item: any) => (

  • {item.title}
  • ))}

);
}
“`

useInfiniteQuery を使うことで、無限スクロールのデータ取得状態(全データ、次のページをフェッチ中、次のページがあるかなど)を簡単に管理できます。

4. Background Refetching (バックグラウンドでの再フェッチ)

前述の通り、TanStack Query はデフォルトで様々なタイミングでバックグラウンドでの再フェッチを行います。

  • Window Focus: ウィンドウにフォーカスが戻ったとき (refetchOnWindowFocus: true – デフォルト)。
  • Reconnect: ネットワークがオンラインに復帰したとき (refetchOnReconnect: true – デフォルト)。
  • Mount: Query を使用するコンポーネントがマウントされたとき (refetchOnMount: true – デフォルト。ただし、データが stale な場合のみ)。
  • Invalidation: invalidateQueries が呼び出されたとき。

これらのバックグラウンドフェッチは、UI をブロックせずに行われ、isFetching プロパティで状態を確認できます。これにより、ユーザーは常に最新のデータに近い状態を見ることができます。必要に応じて、Query オプションでこれらの自動再フェッチを無効化することも可能です。

また、refetchInterval オプションを使って、一定時間ごとにポーリング (polling) を行うこともできます。

“`jsx
import { useQuery } from ‘@tanstack/react-query’;

const fetchStatus = async () => {
const res = await fetch(‘/api/status’); // サーバーの状態などを取得
if (!res.ok) throw new Error(‘Failed to fetch status’);
return res.json();
};

function ServerStatus() {
const { data: status, isFetching } = useQuery({
queryKey: [‘serverStatus’],
queryFn: fetchStatus,
refetchInterval: 5000, // 5秒ごとに自動再フェッチ (ポーリング)
// refetchIntervalInBackground: true, // ウィンドウが非アクティブでもポーリングを続ける場合
});

return (

Server Status: {status || ‘Loading…’}
{isFetching && ‘ (Updating…)’}

);
}
“`

Mutations (useMutation)

データをサーバーで変更する操作(POST, PUT, DELETEなど)には useMutation フックを使用します。useMutation は Query とは異なり、データの変更が主な目的であり、デフォルトではキャッシュを更新しません。

useMutationmutate 関数と、Mutation の状態(isLoading, isError, isSuccess, data, error など)を含むオブジェクトを返します。

useMutation は少なくとも一つの引数、Mutation を実行する非同期関数 (mutationFn) を取ります。

“`jsx
import { useMutation } from ‘@tanstack/react-query’;

// ユーザー作成 API を想定
const createUser = async (userData: { name: string, email: string }) => {
const res = await fetch(‘/api/users’, {
method: ‘POST’,
body: JSON.stringify(userData),
headers: { ‘Content-Type’: ‘application/json’ },
});
if (!res.ok) throw new Error(‘Failed to create user’);
return res.json();
};

function UserForm() {
const mutation = useMutation({
mutationFn: createUser, // 実行する非同期関数
onSuccess: (data, variables, context) => {
// Mutation 成功時のコールバック
console.log(‘User created successfully:’, data);
// ここでキャッシュの無効化などを行うことが多い
},
onError: (error, variables, context) => {
// Mutation 失敗時のコールバック
console.error(‘Error creating user:’, error);
// エラーメッセージ表示などを実装
},
onSettled: (data, error, variables, context) => {
// Mutation 完了時 (成功・失敗に関わらず) のコールバック
console.log(‘User creation finished’);
},
});

const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const name = formData.get(‘name’) as string;
const email = formData.get(‘email’) as string;

// mutate 関数を呼び出して Mutation を実行
// 引数として mutationFn に渡すデータを指定
mutation.mutate({ name, email });

};

return (




{mutation.isError &&

Error: {(mutation.error as Error).message}

}
{mutation.isSuccess &&

User created!

}

);
}
“`

onSuccess, onError, onSettled といったコールバックを使って、Mutation の結果に応じた処理(例えば、ユーザーリストの再フェッチ、成功メッセージ表示、フォームのリセットなど)を実装できます。

楽観的更新 (Optimistic Updates)

Mutation の中でも特に強力で、ユーザー体験を大幅に向上させるのが「楽観的更新」です。これは、Mutation のレスポンスを待たずに、クライアント側でデータの変更を即座に UI に反映させてしまう手法です。サーバーからの実際のレスポンスが成功であればそのまま確定し、失敗であれば元の状態に戻します(ロールバック)。

これにより、ユーザーは操作の結果がすぐに画面に反映されるため、アプリケーションのレスポンス性が高く感じられます。

TanStack Query は、楽観的更新を安全かつ比較的容易に実装するための onMutate コールバックを提供しています。

楽観的更新の一般的な手順:

  1. Mutation が開始される直前 (onMutate) に実行したい処理を定義します。
  2. 現在の Query のキャッシュデータをスナップショットとして保存します(ロールバック用)。
  3. Mutation の引数を使って、対象の Query のキャッシュを手動で新しいであろうデータに更新します (queryClient.setQueryData)。
  4. Mutation が成功 (onSuccess) した場合、スナップショットは不要なので破棄します(または必要なら他の処理を行います)。
  5. Mutation が失敗 (onError) した場合、保存しておいたスナップショットを使って Query のキャッシュを元の状態に戻します (queryClient.setQueryData)。
  6. Mutation の成否に関わらず (onSettled)、関連する Query を無効化 (invalidateQueries) し、サーバーの真のデータに基づいて再フェッチさせます。これにより、楽観的に更新したデータがサーバーの状態とズレていた場合に修正されます。

例: Todo の完了状態をトグルする楽観的更新

“`jsx
import { useMutation, useQueryClient } from ‘@tanstack/react-query’;

interface Todo {
id: number;
title: string;
completed: boolean;
}

// 特定の Todo の完了状態を更新する API を想定
const updateTodoCompleted = async (todoId: number, completed: boolean): Promise => {
const res = await fetch(https://jsonplaceholder.typicode.com/todos/${todoId}, {
method: ‘PUT’,
body: JSON.stringify({ completed }),
headers: { ‘Content-Type’: ‘application/json’ },
});
if (!res.ok) throw new Error(‘Failed to update todo’);
return res.json();
};

function TodoItem({ todo }: { todo: Todo }) {
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: (updatedTodo: Todo) => updateTodoCompleted(updatedTodo.id, updatedTodo.completed),
// Mutation 実行直前
onMutate: async (updatedTodo: Todo) => {
// 1. Mutation 実行前に、関連するクエリの自動再フェッチをキャンセル
// これにより、古いデータがキャッシュに書き戻されるのを防ぐ
await queryClient.cancelQueries({ queryKey: [‘todos’] });

  // 2. 現在の todo リストのキャッシュをスナップショットとして保存
  const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

  // 3. キャッシュを楽観的に更新
  queryClient.setQueryData<Todo[]>(['todos'], oldTodos => {
    return oldTodos
      ? oldTodos.map(t =>
          t.id === updatedTodo.id ? { ...t, completed: updatedTodo.completed } : t
        )
      : undefined; // もしキャッシュが空なら更新しない
  });

  // 4. ロールバック用にスナップショットを context として返す
  return { previousTodos };
},
// Mutation 失敗時 (onMutate で返した context を受け取れる)
onError: (err, newTodo, context) => {
  console.error('Optimistic update failed, rolling back:', err);
  // エラーが発生したら、保存しておいたスナップショットに戻す (ロールバック)
  if (context?.previousTodos) {
    queryClient.setQueryData<Todo[]>(['todos'], context.previousTodos);
  }
  // エラー表示などの追加処理
},
// Mutation 成功時または失敗時 (onMutate で返した context を受け取れる)
onSettled: (data, error, variables, context) => {
  // Mutation が完了したら、念のため todo リストを無効化してサーバーの真の状態に同期
  console.log('Optimistic update settled, invalidating todos');
  queryClient.invalidateQueries({ queryKey: ['todos'] });
},

});

const handleToggleCompleted = () => {
mutation.mutate({ …todo, completed: !todo.completed });
};

return (



  • {todo.title}

    {mutation.isLoading && (Saving…)}
    {mutation.isError && (Error saving)}
  • );
    }
    “`

    楽観的更新は強力ですが、実装はやや複雑になります。特に、onMutate でキャッシュを更新するロジックと、onError でロールバックするロジックを正確に記述する必要があります。しかし、その手間をかける価値は十分にあります。

    Query Client の直接操作

    通常は useQueryuseMutation といったフックを通じて TanStack Query とやり取りしますが、useQueryClient フックを使って Query Client インスタンスを直接取得し、キャッシュを操作したり、Query を手動でフェッチしたりすることも可能です。

    よく使われるメソッド:

    • queryClient.getQueryData(queryKey): 指定した Query Key のキャッシュデータを取得します。
    • queryClient.setQueryData(queryKey, updater): 指定した Query Key のキャッシュデータを手動で設定または更新します。楽観的更新で特に使用します。
    • queryClient.prefetchQuery(options): 指定した Query をバックグラウンドで事前にフェッチし、キャッシュに格納します。ユーザーがそのデータが必要になる前にフェッチしておくことで、待ち時間をなくすことができます。
    • queryClient.invalidateQueries(options): 指定した Query を無効化し、必要に応じて再フェッチをトリガーします。
    • queryClient.removeQueries(options): 指定した Query をキャッシュから削除します。
    • queryClient.cancelQueries(options): 現在進行中の Query をキャンセルします。楽観的更新で Mutation の前に自動再フェッチを止めたい場合などに使用します。

    例: 事前フェッチ (Prefetching)

    ユーザーがリンクをホバーした際に、遷移先のページで必要になるデータを事前にフェッチしておくと、ページ遷移がよりスムーズになります。

    “`jsx
    import { useQueryClient } from ‘@tanstack/react-query’;
    import { Link } from ‘react-router-dom’; // react-router-dom を想定

    const fetchUserDetails = async (userId: number) => {
    const res = await fetch(https://jsonplaceholder.typicode.com/users/${userId});
    if (!res.ok) throw new Error(‘Failed to fetch user’);
    return res.json();
    };

    function UserLink({ userId, userName }: { userId: number; userName: string }) {
    const queryClient = useQueryClient();

    const handleMouseEnter = () => {
    // マウスが乗ったら、ユーザー詳細データを事前にフェッチ
    queryClient.prefetchQuery({
    queryKey: [‘user’, userId],
    queryFn: () => fetchUserDetails(userId),
    staleTime: 1000 * 60 * 10, // 事前フェッチしたデータも10分間 fresh にする
    });
    console.log(Prefetching user ${userId}...);
    };

    return (
    /users/${userId}} onMouseEnter={handleMouseEnter}>
    {userName}

    );
    }
    “`

    この例では、ユーザー名のリンクにマウスが乗った際に、prefetchQuery を使ってそのユーザーの詳細データをバックグラウンドで取得しています。ユーザーが実際にリンクをクリックして詳細ページに遷移した際には、既にデータがキャッシュされているため、即座に表示される可能性が高まります。

    開発者ツールの活用

    繰り返しになりますが、TanStack Query の開発者ツールはデータ取得に関するデバッグと理解を深める上で必須です。

    インストール後、アプリケーションのルートレベルで ReactQueryDevtools コンポーネントを追加することで利用できます。

    開発者ツールで確認できること:

    • Queries: アプリケーション内の全ての Query のリスト。
      • 各 Query の Key。
      • 現在の状態 (Status): 'fetching', 'fresh', 'stale', 'inactive'. 色分けされています。
      • Query が active かどうか。
      • キャッシュデータの内容。
      • Options (staleTime, cacheTime など)。
      • 過去のデータ取得履歴。
    • Mutations: 実行された Mutation のリストとその状態。
    • Query Client Settings: Query Client のデフォルト設定。

    データが期待通りにフェッチされない、キャッシュが効かない、意図しない再フェッチが起きるなどの問題が発生した場合、開発者ツールを見ることで Query の状態やオプション設定がどうなっているか、キャッシュにデータが存在するかどうかなどを瞬時に確認できます。これにより、問題の切り分けが非常に容易になります。

    実際の API ライブラリとの連携 (Fetch / Axios)

    TanStack Query は、データをフェッチする queryFn が Promise を返す関数であれば、どのような API ライブラリやネイティブな fetch API とも連携できます。

    Fetch API との連携:

    先ほどの例で示したように、fetch を使用する場合、レスポンスの ok プロパティを確認してエラーハンドリングを行うのが一般的です。

    ``javascript
    const fetchDataWithFetch = async (url, options) => {
    const res = await fetch(url, options);
    if (!res.ok) {
    // エラーが発生した場合、エラーオブジェクトを throw する
    // このエラーは useQuery の isError/error で捕捉される
    throw new Error(
    HTTP error! status: ${res.status}`);
    }
    return res.json();
    };

    // useQuery の例
    useQuery({
    queryKey: [‘someData’],
    queryFn: () => fetchDataWithFetch(‘/api/data’),
    });
    “`

    Axios との連携:

    Axios を使用する場合も同様に、Promise を返す関数を queryFn に指定します。Axios はデフォルトでエラーレスポンスに対して例外をスローしてくれるため、エラーハンドリングが少しシンプルになります。

    “`javascript
    import axios from ‘axios’;

    const fetchDataWithAxios = async (url, config) => {
    const res = await axios.get(url, config);
    return res.data; // Axios はレスポンスボディを .data に格納
    };

    // useQuery の例
    useQuery({
    queryKey: [‘anotherData’],
    queryFn: () => fetchDataWithAxios(‘/api/another-data’, { headers: { ‘X-Custom’: ‘…’ } }),
    });

    // mutationFn の例 (POST)
    const postDataWithAxios = async (data) => {
    const res = await axios.post(‘/api/create-item’, data);
    return res.data;
    };

    useMutation({
    mutationFn: postDataWithAxios,
    onSuccess: () => { // },
    });
    “`

    どちらの場合も、重要なのは queryFn または mutationFnデータを解決する Promise を返す こと、そして エラー発生時に Promise を reject する(例外をスローする) ことです。TanStack Query はこの Promise の状態変化(pending, resolved, rejected)を監視して、内部の状態を更新します。

    TypeScript との連携

    TanStack Query は TypeScript で強く型付けされており、ジェネリクスを使用することで取得するデータ、エラー、Mutation の変数、コンテキストなどに型情報を与えることができます。これにより、開発体験がさらに向上し、型エラーを防ぐことができます。

    useQuery, useMutation, queryClient.setQueryData など、多くのフックやメソッドがジェネリクスをサポートしています。

    例: useQuery に型情報を与える

    “`typescript
    interface User {
    id: number;
    name: string;
    email: string;
    }

    const fetchUsers = async (): Promise => { // Promise の解決型を指定
    const res = await fetch(‘https://jsonplaceholder.typicode.com/users’);
    if (!res.ok) {
    // エラー型を指定することも可能 (例: string, Error, CustomError)
    throw new Error(‘Failed to fetch users’);
    }
    return res.json();
    };

    function UserList() {
    // useQuery<取得データの型, エラーの型, select後のデータの型, QueryKeyの型>
    const { data: users, isLoading, isError, error } = useQuery<
    User[], // 取得データの型
    Error // エラーの型
    // select を使っていないので、第3引数は省略
    // QueryKey の型は推論されることが多いが、明示することも可能

    ({
    queryKey: [‘users’],
    queryFn: fetchUsers,
    });

    if (isLoading) return

    Loading users…

    ;
    // error は Error 型として扱えるようになる
    if (isError) return

    Error fetching users: {error.message}

    ;

    return (

      {/ users は User[] 型として扱えるようになる /}
      {users?.map(user => (

    • {user.name}
    • ))}

    );
    }
    “`

    useMutation も同様に型付けできます。

    “`typescript
    interface NewTodo {
    title: string;
    // userId など
    }

    interface CreatedTodo {
    id: number;
    title: string;
    completed: boolean;
    // userId など
    }

    const addTodo = async (newTodo: NewTodo): Promise => { // 引数と戻り値に型
    const res = await fetch(‘…’); // POST リクエスト
    if (!res.ok) throw new Error(‘…’);
    return res.json();
    };

    function TodoForm() {
    // useMutation<成功時のデータの型, エラーの型, mutationFnの第1引数の型, onMutateのcontextの型>
    const mutation = useMutation({
    mutationFn: addTodo,
    onSuccess: (data) => {
    // data は CreatedTodo 型
    console.log(‘Added:’, data);
    },
    onError: (error) => {
    // error は Error 型
    console.error(‘Error:’, error.message);
    },
    // context の型を指定する場合 onMutate を使用
    // onMutate: async (variables: NewTodo): Promise<{ previousTodos: Todo[] | undefined }> => { … }
    // onError: (err, vars, context) => { if (context?.previousTodos) … }
    });

    // … フォームの onSubmit ハンドラ内で mutation.mutate({ title: ‘…’ }) を呼び出す
    }
    “`

    TypeScript と共に使うことで、API から返されるデータの構造や Mutation の引数・戻り値の型安全性が保証され、開発中のエラーを減らすことができます。

    ベストプラクティスとヒント

    • Query Key は常に配列で構造化する: ['todos', { status: 'completed' }], ['user', userId], ['products', { category: 'electronics', page: 1 }] のように、データの種類とそれに続くパラメータを要素として含む配列にしましょう。これにより、キャッシュの管理や無効化が論理的に行えます。
    • カスタムフックにまとめる: 各 API エンドポイントに対する Query や Mutation の定義を、useUsers, usePost, useAddTodo のようなカスタムフックにまとめましょう。これにより、コンポーネントはデータ取得ロジックの詳細を知る必要がなくなり、コードの再利用性と可読性が向上します。

      “`javascript
      // hooks/useUsers.js
      import { useQuery } from ‘@tanstack/react-query’;

      const fetchUsers = async () => { // };

      export function useUsers() {
      return useQuery({
      queryKey: [‘users’],
      queryFn: fetchUsers,
      });
      }

      // components/UserList.js
      import { useUsers } from ‘../hooks/useUsers’;

      function UserList() {
      const { data: users, isLoading, isError } = useUsers();
      // … レンダリングロジック
      }
      ``
      * **
      selectオプションを活用して必要なデータだけ取得する:** コンポーネントがキャッシュされたデータ全体ではなく、その一部だけを必要とする場合はselectオプションを使ってデータを整形しましょう。これにより、関連性のないデータ変更によるコンポーネントの不必要な再レンダリングを防ぐことができます。
      * **エラーハンドリング戦略を検討する:**
      * Query レベル:
      useQueryisErrorerrorを使って、コンポーネント内でエラーメッセージを表示する。
      * Mutation レベル:
      useMutationonErrorコールバックでエラーを処理し、トースト通知を表示するなど。
      * グローバルレベル:
      QueryClientdefaultOptionsで Query や Mutation のonErrorに共通のエラーハンドリング関数を設定する。
      * **ローディング状態の表示:**
      *
      isLoading: 初回フェッチ中の表示に使う。
      *
      isFetching: バックグラウンドでの更新も含めた全てのフェッチ中に使う。isLoadingの間もisFetchingtrueです。isFetching && !isLoadingはバックグラウンドフェッチ中を示します。
      *
      isPreviousData(ページネーション): 新しいデータをフェッチ中だが前のデータを表示している状態を示す。
      *
      isFetchingNextPage(無限スクロール): 次のページをフェッチ中を示す。
      これらのプロパティを組み合わせて、ユーザーが待たされている状況を適切にフィードバックしましょう。
      * **
      staleTimecacheTimeを適切に設定する:** アプリケーションのデータの性質に合わせてこれらのキャッシュ設定を調整します。頻繁に更新されるデータはstaleTimeを短く、ほとんど更新されない静的なデータはstaleTime: Infinityにすることも考えられます。cacheTimeはメモリ使用量に影響します。デフォルト設定 (staleTime: 0,cacheTime: 5分`) は多くのケースで良い出発点となります。
      * 開発者ツールを最大限に活用する: デバッグ時の第一歩は開発者ツールを開くことです。Query や Mutation の状態、キャッシュの内容、オプション設定などを確認し、問題の原因を特定しましょう。

    よくある落とし穴と対処法

    • Query Key が正しくない: Query Key が一意でない、またはフェッチ関数に必要なパラメータをキーに含めていない場合、キャッシュが正しく機能しない、意図しないデータが取得されるといった問題が発生します。Query Key はフェッチ関数の引数と論理的に一致させる必要があります。
    • queryFn が Promise を返さない、またはエラーをスローしない: queryFn は Promise を返し、エラー時には Promise を reject するか例外をスローする必要があります。これを怠ると、TanStack Query はローディング状態から抜け出せなくなったり、エラーを捕捉できなかったりします。
    • QueryClientProvider を忘れる: アプリケーションツリーの上位で QueryClientProvider を設定していないと、useQuery などのフックを使用した際にエラーになります。
    • 無限ループ: useQueryqueryFn の中で、Query Key の依存関係を変更するようなステート更新を行うと、無限ループに陥る可能性があります。queryFn は副作用のない関数として、Query Key に基づいてデータフェッチのみを行うように設計すべきです。
    • Mutation 後のデータ更新忘れ: useMutation の成功後に、関連する Query のキャッシュを invalidateQueries で無効化したり、setQueryData で手動更新したりしないと、UI に古いデータが表示され続けます。
    • 依存配列なしで useEffect を使ってフェッチする癖が抜けない: TanStack Query を使う場合、データフェッチは useQuery に任せましょう。useEffect を使って手動フェッチを行うのは、TanStack Query で管理するサーバー状態ではなく、一時的なローカルデータのフェッチや、Query Client の操作(例: 特定イベントでの invalidateQueries 呼び出し)などの場合に限定すべきです。

    他のデータ取得ライブラリとの比較 (SWR, Apollo Clientなど)

    データ取得ライブラリには TanStack Query の他に SWR や Apollo Client などがあります。簡単に比較してみましょう。

    • SWR: Vercel が提供するライブラリで、TanStack Query と同様に stale-while-revalidate 戦略に基づいています。API は TanStack Query よりも少しシンプルで、軽量です。シンプルなユースケースには非常に適しています。ただし、TanStack Query の方が高機能で、キャッシングの制御オプション、開発者ツール、無限スクロール、楽観的更新などの機能がより充実しています。複雑なサーバー状態管理が必要な場合は TanStack Query が優位に立つことが多いです。
    • Apollo Client / Relay: これらは GraphQL クエリに特化したライブラリです。TanStack Query や SWR が REST やカスタム API など、API の種類を問わないのに対し、Apollo や Relay は GraphQL スキーマに基づいてクライアント側のデータ管理を深く統合します。クライアントの状態管理機能も内包していることが多いです。GraphQL を全面的に採用しているプロジェクトであれば強力な選択肢ですが、REST API やその他の API を使用する場合は TanStack Query がより適しています。TanStack Query は GraphQL にも利用可能ですが、GraphQL 固有の機能(例: フラグメント管理)は Apollo などの方が充実しています。

    TanStack Query は、API の種類に依存せず、堅牢で高機能なサーバー状態管理ソリューションを提供したい場合に非常にバランスの取れた選択肢と言えます。

    まとめ:TanStack Query がもたらす快適な開発体験

    この記事では、React におけるデータ取得の課題と、TanStack Query がそれらをどのように解決するのかを詳細に見てきました。

    TanStack Query は単なるデータフェッチライブラリではなく、サーバー状態管理のための包括的なソリューションです。

    • useEffect と手動ステート管理による boilerplate からの解放
    • 強力な自動キャッシュによるパフォーマンスと UX の向上
    • stale-while-revalidate モデルによる常に新鮮なデータと高速な初期表示の両立
    • バックグラウンド更新、フォーカス時のリフレッシュなどの自動化
    • Query Key による宣言的なキャッシュ管理とデータ共有
    • Mutation によるデータの変更と、楽観的更新を含む洗練された更新パターン
    • ページネーション、無限スクロール、ポーリングといった複雑な機能の容易な実装
    • 強力な開発者ツールによるデバッグの効率化
    • API ライブラリやデータ構造に依存しない汎用性
    • TypeScript による型安全性の向上

    これらの機能により、React 開発者はデータ取得やキャッシュ管理といった面倒なタスクに時間を費やすことなく、アプリケーションの UI やビジネスロジックといったより重要な部分に集中できるようになります。これは開発効率の向上だけでなく、アプリケーションの信頼性やユーザー体験の向上にも直結します。

    もしあなたが React でデータ取得に苦労しているなら、ぜひ TanStack Query を試してみてください。一度その快適さを知れば、もう手動でのデータ管理には戻れないはずです。

    TanStack Query の公式ドキュメントは非常に充実していますので、さらに深く学びたい場合はそちらも参照してください。

    快適なデータ取得ライフを!

    コメントする

    メールアドレスが公開されることはありません。 が付いている欄は必須項目です

    上部へスクロール