react query入門:キャッシュ設計から運用ルールまで徹底解説

この記事では、TanStack Query(React Query)の導入から、Axios/SWRとの違い、staleTime・cacheTime(gc)によるキャッシュ挙動、invalidateQueries等の更新戦略、エラーハンドリングや二重送信防止、ページネーション対応までを整理し、データ取得の設計迷いと性能・保守性の悩みを解決します。

目次

React Query(TanStack Query)とは何か

react+query+cache

React Query(現在は「TanStack Query」としても知られる)は、Reactアプリケーションにおける「サーバー由来の状態(Server State)」を扱うためのデータフェッチング/キャッシュ管理ライブラリです。APIから取得したデータを、ただ表示するだけでなく、キャッシュ・再取得・同期・更新後の整合性維持までを一貫して支援します。

Reactのコンポーネント内でfetchaxiosを直接呼ぶ実装はシンプルに始められる一方、画面数やデータ依存が増えるほど「いつ再取得するか」「同じデータをどこで共有するか」「ローディングやエラーの扱いを統一するか」といった問題が顕在化します。react queryは、これらをアプリの共通基盤として整理し、UI実装を“データの状態に基づく描画”へ寄せられる点が大きな価値です。

解決できる課題と利用シーン

react queryが特に効くのは、「サーバーのデータを表示・更新する画面が多い」プロダクトです。手書き実装で起こりがちな課題を、設計として吸収できます。

  • 同じAPIを複数画面で叩いてしまい、無駄な通信が増える
    一覧画面と詳細画面、ダッシュボードとサイドバーなど、同一データを複数箇所で参照すると二重取得が起こりやすくなります。react queryはキャッシュを中心に据えることで、同一データの再利用を自然に行えます。

  • ローディング/エラー/成功の状態管理が画面ごとにバラバラ
    「ロード中はスピナー」「失敗時はリトライボタン」などのUI要件が増えるほど、状態管理の統一が難しくなります。react queryは取得状態を標準的な形で提供し、UIの分岐を整理しやすくします。

  • 更新後の再取得・反映漏れが起きる
    例えば、プロフィール更新後にヘッダーのユーザー名が古いまま残る、といった“整合性の崩れ”はよくある不具合です。react queryは更新(ミューテーション)と再同期の流れを組み立てやすくし、反映漏れを減らします。

  • 画面遷移やタブ切り替えで「いつ再取得すべきか」が曖昧
    ユーザー操作やフォーカス復帰、ネットワーク復旧など、再取得のトリガーは多岐にわたります。react queryは“再取得の標準動作”を用意し、必要に応じて制御できます。

利用シーンとしては、管理画面(一覧・詳細・編集が多い)、EC(カート/在庫/注文状況など同期が重要)、SaaSダッシュボード(複数ウィジェットが同時にデータを参照する)などが代表例です。

主な特徴と強み(キャッシュ・同期・状態管理)

react queryの核は「サーバー状態を、キャッシュと同期の仕組みで扱う」点にあります。単なるデータ取得ヘルパーではなく、アプリのデータ体験(速さ・正しさ・一貫性)を上げる設計が特徴です。

  • キャッシュを前提とした高速なデータ参照
    一度取得したデータをキャッシュし、同じ条件のデータ要求に対してはキャッシュを返せます。これにより「戻る操作」「再表示」「別コンポーネントでの参照」でも体感が速くなり、APIの負荷も抑えやすくなります。

  • サーバーとの同期(再取得・更新の整合性)を支援
    サーバーデータはクライアントだけで完結せず、他ユーザー操作や時間経過で変化します。react queryは“最新化”のための仕組みを持ち、画面表示とデータのズレを小さくしやすいのが強みです。

  • 非同期処理の状態管理(Loading/Error/Success)を標準化
    手実装では「ローディングフラグ」「エラー内容」「リトライ」などが散らばりがちですが、react queryでは取得結果に紐づく状態が揃った形で扱えます。結果として、UIが“状態に従って描画する”設計に寄り、保守性が上がります。

  • Reactのレンダリングモデルと相性のよいデータ購読
    コンポーネントが「必要なデータ」を宣言的に購読し、更新に応じて再描画される形を取りやすいのも特徴です。データ取得ロジックをUIから分離しやすく、画面の見通し改善に繋がります。

まとめると、react queryは「API通信をする」ための道具というより、「サーバー由来データをアプリ全体で正しく・速く・一貫して扱う」ための基盤として機能します。

Axios・SWRとの位置づけと違い

react queryを理解するうえで重要なのは、AxiosやSWRとは“同じレイヤーの代替”ではない点です。役割の違いを押さえると、採用判断がクリアになります。

  • Axios:HTTPクライアント(通信手段)
    Axiosはリクエスト/レスポンス、ヘッダー付与、インターセプターなどを扱う「HTTPクライアント」です。データ取得後のキャッシュ戦略や再取得ルール、状態管理までを包括的に提供するものではありません。つまり、Axiosはreact queryと競合するというより、react queryの“データ取得関数の中でAxiosを使う”形で共存しやすい立ち位置です。

  • SWR:データフェッチ + キャッシュの軽量アプローチ
    SWRもキャッシュと再検証(stale-while-revalidate)の思想で、Reactのデータ取得を簡潔にします。対してreact query(TanStack Query)は、クエリ/ミューテーションを軸に、より広いケース(更新後の整合性、複雑なデータ依存、細かな制御)に対応しやすい設計です。どちらが優れているというより、求める運用規模や制御レベルで選び分けるのが現実的です。

  • React Query(TanStack Query):サーバー状態管理レイヤー
    react queryは「取得して終わり」ではなく、取得したデータをアプリの資産として扱い、再利用・同期・更新整合性を体系化します。画面が増えたときに効いてくるのは、通信そのものよりも“データの扱い方のルール化”であり、その部分に強みがあります。

整理すると、Axiosは“どう通信するか”、SWR/React Queryは“取得したデータをUIとどう結びつけ、どう最新化するか”が主戦場です。react queryは特に、アプリが成長したときの運用を見据えて、キャッシュと同期を中核に据えたいケースで選ばれやすい選択肢です。

導入前に知っておきたい基礎概念(キャッシュとライフサイクル)

react+query+caching

React Query(TanStack Query)は「サーバー状態」を扱うためのライブラリであり、導入効果の大半はキャッシュライフサイクルの理解で決まります。特に重要なのが、データが「新鮮か古いか」を判定するstale、使われなくなったキャッシュをいつ捨てるかを決めるgc(ガベージコレクション)、そしてキャッシュの紐づけ単位であるqueryKeyです。

このセクションでは、React Queryの挙動を「なぜそう動くのか」まで腹落ちさせるために、キャッシュの状態遷移を軸に基本概念を整理します。

stale(新鮮/古い判定)の考え方

React Queryでまず押さえるべきは、キャッシュが常に「正しい」わけではなく、一定時間を過ぎると“古い(stale)”とみなされる点です。ここでいうstaleは「誤っている」ではなく、再取得して最新化する余地がある状態を意味します。

stale判定は主にstaleTimeによって決まります。

  • fresh(新鮮):取得直後〜staleTimeが切れるまで。基本的に「再取得しなくてよい」扱い。
  • stale(古い)staleTime経過後。キャッシュは返せるが「状況により再取得する」扱い。

重要なのは、React Queryはstaleになった瞬間に即座に必ずリフェッチするのではなく、“再取得のトリガー”が発生したときに再取得しやすくなる、という設計思想です。トリガーの代表例は「コンポーネントのマウント」「ウィンドウのフォーカス復帰」などですが、ここでは詳細ではなく、staleが“再取得の判断材料”であることを押さえてください。

つまり、React Queryでは次のような読み替えができます。

  • staleTimeを長くする=「このデータはしばらく最新扱いでよい」=再取得頻度を下げやすい
  • staleTimeを短く(あるいは0)=「すぐ古い扱いでよい」=機会があれば再取得しやすい

React Queryを導入して「想定よりリフェッチが多い/少ない」と感じたとき、最初に疑うべきがこのstaleの設計です。

gc(キャッシュ破棄)と保持期間の考え方

次に理解すべきは、キャッシュがメモリ上にいつまでも残り続けないよう、React Queryが不要になったキャッシュを破棄する仕組みを持っている点です。これがgc(ガベージコレクション)で、保持期間は一般にgcTime(旧称cacheTime)で制御します。

ポイントは、キャッシュが破棄されるのは「staleになったから」ではなく、そのクエリが非アクティブになってから一定時間が経過したときということです。

  • アクティブ:そのクエリを参照する購読者(通常はコンポーネント)が存在する状態
  • 非アクティブ:購読者がいなくなり、キャッシュだけが残っている状態

非アクティブになった瞬間に消えるわけではなく、gcTimeの間は「また必要になったら即返せる」ように保持されます。そしてgcTimeを超えると、メモリ節約のために破棄され、次回は初回取得と同じようにネットワークアクセスが必要になります。

この挙動を理解すると、次のような設計判断がしやすくなります。

  • 画面遷移で戻ってくる可能性が高いデータ:gcTimeを長めにして体感速度を上げる
  • 重い一覧や巨大レスポンス:gcTimeを短めにしてメモリを守る

注意:「stale(古い)」と「gc(破棄)」は別概念です。staleは再取得判断、gcはメモリから消す判断であり、混同すると意図しない再取得やキャッシュ消失の原因になります。

queryKeyでキャッシュを管理する基本

React Queryのキャッシュは、queryKeyをキーとして管理されます。つまり、同じqueryKeyなら同じキャッシュを共有し、違うqueryKeyなら別のキャッシュとして扱われます。ここが設計の要で、React Queryの挙動が「予想通り」になるかどうかはqueryKey設計で大きく左右されます。

基本形は配列で、次のように「リソース名 + 条件」を表現します。

  • 例:['users'](ユーザー一覧)
  • 例:['user', userId](特定ユーザー)
  • 例:['users', { page: 1, keyword: 'a' }](一覧 + 検索条件)

設計上の重要ポイントは次のとおりです。

  • 条件が違うなら必ずqueryKeyも変える:ページ番号や検索語などが同じキャッシュに混ざるのを防ぐ
  • 同じ意味なら同じqueryKeyに寄せる:同一データを無駄に重複取得しない
  • オブジェクト条件は安定性を意識する:毎回“別物”と判定されないよう、同一条件なら同一構造にする(意図せぬキャッシュ分裂を避ける)

React Queryは「URL」ではなく「queryKey」で世界が回っている、と捉えると理解が早いです。

useQueryの基本コードで全体像を掴む

ここまでの概念(stale / gc / queryKey)を、useQueryの最小コードで結びつけます。React Queryの基本は「queryKeyでキャッシュを特定し、queryFnで取得し、staleとgcのルールで再利用・再取得・破棄を行う」という流れです。

import { useQuery } from '@tanstack/react-query'

function UserDetail({ userId }: { userId: string }) {
  const query = useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const res = await fetch(`/api/users/${userId}`)
      if (!res.ok) throw new Error('Failed to fetch')
      return res.json()
    },
    staleTime: 60_000, // 60秒は新鮮扱い
    gcTime: 10 * 60_000, // 非アクティブ後10分で破棄(例)
  })

  if (query.isLoading) return <p>Loading...</p>
  if (query.isError) return <p>Error</p>

  return <p>{query.data.name}</p>
}

この例では、['user', userId]がキャッシュの住所になります。同じuserIdを表示する別コンポーネントが現れた場合、同じqueryKeyなら同じキャッシュを使い回します。さらにstaleTime以内なら「新鮮扱い」、非アクティブになってもgcTime内ならキャッシュが残り、復帰時に即表示しやすくなります。

初回取得時の挙動(初回マウント)

コンポーネントが初めてマウントされ、該当するqueryKeyのキャッシュが存在しない場合、React Queryは次の順で動きます。

  1. キャッシュ未存在:返せるデータがない
  2. queryFnを実行して取得開始:ネットワークアクセスが発生
  3. 取得成功でキャッシュに保存:以後は同じqueryKeyで再利用可能
  4. 保存直後はfreshstaleTimeの間は新鮮扱い

つまり「初回だけ遅いが、同じデータを使い回せる」のがReact Queryの基本価値です。初回にローディングが出るのは自然な挙動で、以後の体感をキャッシュで改善します。

同一クエリ再実行時の挙動(再レンダリング/再マウント)

同じqueryKeyで再度useQueryが呼ばれた場合、React Queryはまずキャッシュを確認します。このときの挙動は「キャッシュがあるか」と「staleかどうか」で分岐します。

  • キャッシュあり & fresh:基本的にキャッシュを即返す(体感が速い)
  • キャッシュあり & stale:キャッシュは返せるが、状況により再取得が走り得る(最新化の余地)

ここで重要なのは、React Queryが「毎回必ず取り直す」のではなく、キャッシュを起点に表示を成立させ、必要に応じて裏で更新する方向に最適化されている点です。結果として、再レンダリングや再マウントが発生しても、同一クエリなら“都度フルローディング”になりにくくなります。

アンマウント後の挙動(非活性化と破棄まで)

コンポーネントがアンマウントされると、そのクエリを購読している存在が減ります。購読者がゼロになるとクエリは非アクティブとなり、ここからgcTimeのカウントが始まります。

非アクティブ期間の挙動は次のとおりです。

  1. アンマウント:購読者がいなくなる
  2. 非アクティブ化:キャッシュは残る(すぐには消えない)
  3. gcTime内に再マウント:同じqueryKeyならキャッシュが即利用されやすい
  4. gcTime超過:キャッシュが破棄され、次回は初回取得と同様の動きに戻る

このライフサイクルを理解しておくと、「画面を離れて戻ったら即表示された/またローディングになった」といった現象を、React Queryの仕様として説明できるようになります。運用上は、戻りやすい導線の画面ほどgcTimeが体感に直結しやすい、と覚えておくと判断がスムーズです。

セットアップ手順(React/React Native)

react+query+cache

React Query(TanStack Query)をプロジェクトに導入する最初のステップは、「QueryClientを作成し、アプリ全体にProviderとして組み込む」ことです。ここまで済ませれば、各画面・各コンポーネントから一貫した設定でクエリを実行でき、キャッシュも共有されます。このセクションでは、React/React Nativeのどちらでも通用する基本セットアップを、最小構成から順に整理します。

インストールと初期構成(QueryClientの用意)

まずはReact Query本体(TanStack Query)をインストールします。パッケージ名は@tanstack/react-queryです。

# npm
npm i @tanstack/react-query

# yarn
yarn add @tanstack/react-query

# pnpm
pnpm add @tanstack/react-query

次に、アプリで共有する「QueryClient(クエリの実行・キャッシュ管理の中心)」を作成します。基本はアプリ起動中に同一インスタンスを使い回すため、コンポーネントのレンダリングのたびに新規生成しない位置に置くのが重要です(例:src/queryClient.tsのような専用ファイル)。

import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 必要最小限の例。詳細なチューニングは運用方針で行う
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});

初期構成で押さえるポイントは次の通りです。

  • QueryClientは「キャッシュとリクエスト状態」を統括するため、アプリで基本1つを共有します。
  • defaultOptionsを設定しておくと、各useQueryに同じオプションを繰り返し書かずに済みます。
  • React Nativeでは「Windowフォーカス」がブラウザと同じではないため、refetchOnWindowFocusは要件に応じて明示しておくと意図がブレにくくなります。

QueryClientProviderの組み込み

作成したQueryClientをアプリ全体で利用できるようにするには、ルート付近でQueryClientProviderを配置します。これにより、配下のコンポーネントからReact Queryのフックが利用可能になり、キャッシュも共有されます。

React(Web)の例:

import React from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './queryClient';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

React Nativeの例(エントリポイントや構成によりファイルは異なりますが、考え方は同じです):

import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './src/queryClient';
import { AppRoot } from './src/AppRoot';

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AppRoot />
    </QueryClientProvider>
  );
}

Provider組み込み時の注意点は以下です。

  • Providerの外側でReact Queryのフックを呼ぶと、実行時エラーになります。
  • QueryClientをコンポーネント内で生成すると、再レンダリングでクライアントが作り直され、キャッシュが意図せずリセットされる原因になります。
  • ルーティング(React Routerなど)を使う場合も、基本はルーターより外側(=より上位)にProviderを置くと、画面遷移でもキャッシュを共有しやすくなります。

Devtoolsでクエリを可視化する設定

React Queryの導入初期〜運用フェーズで効くのがDevtoolsです。現在のクエリ一覧、キャッシュ内容、フェッチ状態などを可視化でき、想定外の再取得やキャッシュの残り方を素早く確認できます。Web(React)では公式のDevtoolsを組み込むのが定番です。

まずDevtoolsを追加インストールします。

# npm
npm i -D @tanstack/react-query-devtools

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

# pnpm
pnpm add -D @tanstack/react-query-devtools

次に、QueryClientProvider配下にDevtoolsを配置します。開発環境のみ有効化するのが一般的です。

import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from './queryClient';
import App from './App';

export function Root() {
  return (
    <QueryClientProvider client={queryClient}>
      <App />
      {process.env.NODE_ENV === 'development' ? (
        <ReactQueryDevtools initialIsOpen={false} />
      ) : null}
    </QueryClientProvider>
  );
}

Devtools設定で意識するとよい点は次の通りです。

  • initialIsOpenで初期表示(開く/閉じる)を調整できます。まずは閉じた状態から必要時に開く運用が無難です。
  • 本番ビルドにDevtoolsを含めないよう、環境分岐(developmentのみ)を入れておくと安心です。
  • React NativeではWebのようなDevtools表示が前提にならないケースもあるため、まずはReact(Web)で動作とキャッシュの見え方を掴み、必要に応じて環境に合ったデバッグ手段を検討します。

基本の使い方(クエリ)

react+query+cache

useQueryでデータ取得する流れ

React Query(TanStack Query)でのデータ取得は、コンポーネント内でuseQueryを呼び出し、取得結果と状態(ローディング/成功/失敗)をフックから受け取って描画に反映する、という流れが基本です。ポイントは「取得処理を自前でuseEffect管理しない」ことと、「同じクエリはキャッシュを介して再利用される」ことです。

最小構成は、queryKeyqueryFn(実際に取得する関数)を渡すだけです。例えばユーザー一覧を取得する場合は次のようになります。

import { useQuery } from '@tanstack/react-query'

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

async function fetchUsers(): Promise<User[]> {
  const res = await fetch('/api/users')
  if (!res.ok) throw new Error('Failed to fetch users')
  return res.json()
}

export function UserList() {
  const query = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  // query.data に結果が入る(成功時)
  // query.isLoading / query.isError などで状態を判定できる
  return (
    <div>
      {/* 表示は次のセクションで詳述 */}
      {query.data?.map((u) => (
        <div key={u.id}>{u.name}</div>
      ))}
    </div>
  )
}

また、URLパラメータや検索条件など「入力によって取得対象が変わる」ケースでは、queryKeyにその条件を含めるのが定石です。これにより、条件ごとに別キャッシュとして扱われ、意図しないデータ混在を防げます。

export function UserDetail({ userId }: { userId: string }) {
  const query = useQuery({
    queryKey: ['users', userId],
    queryFn: async () => {
      const res = await fetch(`/api/users/${userId}`)
      if (!res.ok) throw new Error('Failed to fetch user')
      return res.json()
    },
    enabled: !!userId, // userId が無い間は取得しない
  })

  return <div>{query.data?.name}</div>
}

enabledは「前提条件がそろうまでクエリを実行しない」ための実用的なオプションです。React Queryを使うと、こうした「条件付き取得」もフックの宣言として読みやすく表現できます。

ローディング・成功・失敗の状態管理

React Queryでは、データ取得の状態管理がuseQueryから返されるフラグで完結します。自前のuseStateloadingを持ったり、try/catchで詰めた状態遷移を組む必要が減り、UI分岐が素直になります。

代表的な状態フラグは以下です。

  • isPending/isLoading:初回取得など「まだデータが無い状態」で読み込み中
  • isFetching:バックグラウンド再取得も含めた「取得中」全般
  • isSuccess:取得成功(dataが利用可能)
  • isError:取得失敗(errorが利用可能)

UIでは、まず「初回ローディング」と「エラー」を分岐し、その後に成功時の表示を行うパターンが一般的です。バックグラウンド更新中であっても既存データを表示し続けたい場合は、isFetchingを使って「小さなスピナーだけ出す」といった表現ができます。

export function UserList() {
  const { data, isLoading, isError, error, isFetching } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  if (isLoading) return <p>読み込み中...</p>

  if (isError) {
    return <p>取得に失敗しました: {(error as Error).message}</p>
  }

  return (
    <div>
      {isFetching && <p>更新中...</p>}
      <ul>
        {data!.map((u) => (
          <li key={u.id}>{u.name}</li>
        ))}
      </ul>
    </div>
  )
}

このように、React Queryは「成功データ(data)」「失敗情報(error)」「進行状態(isLoading/isFetching)」が揃っており、状態ごとのUI設計を一箇所に閉じ込めやすいのが利点です。

エラー処理の実装パターン

React Queryのエラー処理は、基本的に「queryFnが例外を投げる(Promiseをrejectする)とisErrorになり、errorに格納される」というモデルです。したがって、fetchを使う場合はres.okを確認し、失敗時に例外化するのが重要です(fetchはHTTPエラーでもPromiseがrejectされないため)。

async function fetchUsers() {
  const res = await fetch('/api/users')
  if (!res.ok) {
    // APIが返すエラー形式に応じてメッセージ整形してもよい
    throw new Error(`HTTP ${res.status}`)
  }
  return res.json()
}

実装パターンとしては、主に次の3つが使われます。

  • パターン1:コンポーネント内で分岐して表示
    最もシンプルで、画面ごとにエラー表示を最適化できます。エラーメッセージをそのまま表示するのではなく、ユーザー向け文言に置き換えるのが実運用では重要です。

  • パターン2:再試行導線(リトライボタン)を置く
    一時的なネットワーク不調に備えて、ユーザーが手動で再取得できる導線を用意します。refetchを呼べばそのクエリだけを再実行できます。

    export function UserList() {
      const { data, isError, error, isLoading, refetch, isFetching } = useQuery({
        queryKey: ['users'],
        queryFn: fetchUsers,
      })
    
      if (isLoading) return <p>読み込み中...</p>
    
      if (isError) {
        return (
          <div>
            <p>取得に失敗しました: {(error as Error).message}</p>
            <button onClick={() => refetch()} disabled={isFetching}>
              再試行
            </button>
          </div>
        )
      }
    
      return <ul>{data!.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
    }
  • パターン3:Error Boundary(境界)に委ねる
    画面全体で一律のエラーUIにしたい場合、ReactのError Boundaryに寄せる設計が有効です。React Query側でthrowOnErrorなどを使い、エラーを投げて境界で捕捉する構成にできます。画面内で細かく分岐しないため、UIの統一と実装の簡略化に寄与します。

どのパターンでも、ユーザーにとって必要なのは「何が起きたか」「次にどうすればいいか」です。React Queryのerrorは開発向け情報になりやすいので、そのまま表示せず、文言・再試行・問い合わせ導線などとセットで設計するのが実践的です。

ウィンドウフォーカス時の再取得(自動リフェッチ)

React Queryには、ブラウザのタブが再びアクティブになった(ウィンドウフォーカスが戻った)タイミングで、データを自動的に再取得する仕組みがあります。これにより「別タブで作業して戻ってきたら情報が古い」という問題を軽減できます。

この挙動はrefetchOnWindowFocusで制御できます。デフォルト挙動はプロジェクト設定に依存しますが、クエリ単位で明示する場合は次のように書けます。

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  refetchOnWindowFocus: true,   // フォーカス時に再取得する
})

逆に、フォーカス時の再取得が不要(あるいはコストが高い)な画面では無効化します。例えば、頻繁な再取得がAPI負荷やレート制限に直結するケース、入力中フォームと同居していて更新がノイズになるケースなどです。

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  refetchOnWindowFocus: false,  // フォーカス時に再取得しない
})

また、「毎回必ず再取得」ではなく、状況に応じて条件付きにしたい場合は関数指定も可能です。例えば「エラー中はフォーカスで再試行したい」「特定画面だけ抑制したい」といったポリシーをクエリごとに表現できます。

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  refetchOnWindowFocus: (query) => {
    // 例: エラー状態ならフォーカスで再試行する
    return query.state.status === 'error'
  },
})

ウィンドウフォーカス時の自動リフェッチは、React Queryらしい「ユーザー行動に合わせて最新化する」機能です。必要な画面にだけ適用し、不要な画面では抑制することで、UXと負荷のバランスを取りやすくなります。

更新処理(ミューテーション)とキャッシュ整合性

react+query+cache

react query(TanStack Query)で「更新(作成・変更・削除)」を扱うときの主役がミューテーションです。更新自体はサーバーへ送る“書き込み”ですが、画面が参照しているのは多くの場合クエリのキャッシュです。つまり、更新に成功した瞬間に「キャッシュとサーバーの状態をどう一致させるか(整合性)」まで設計して初めて、UIが正しく安定して動きます。

useMutationの基本

useMutationは、POST/PUT/PATCH/DELETE などの“副作用を伴う処理”を実行し、その実行状態(送信中・成功・失敗)を管理するためのフックです。react queryでは、取得はuseQuery、更新はuseMutationと役割分担すると見通しが良くなります。

基本形は「ミューテーション関数(mutationFn)」を渡し、必要に応じて成功/失敗時のコールバックを追加します。

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

type UpdateTodoInput = { id: string; title: string };
type Todo = { id: string; title: string; completed: boolean };

async function updateTodo(input: UpdateTodoInput): Promise<Todo> {
  const res = await fetch(`/api/todos/${input.id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title: input.title }),
  });
  if (!res.ok) throw new Error("Failed to update");
  return res.json();
}

export function useUpdateTodo() {
  return useMutation({
    mutationFn: updateTodo,
  });
}

// 利用側
// const { mutate, mutateAsync, isPending, isError, error, data } = useUpdateTodo();
// mutate({ id, title });

mutateはコールバックスタイル、mutateAsyncawaitできるPromiseスタイルです。フォーム送信のように「成功後に次の処理へ進めたい」場合はmutateAsyncが扱いやすく、ボタン連打防止などUI連動にはisPendingが有効です。

更新後にクエリを無効化して再取得する

更新後のキャッシュ整合性で最も堅実なのが「関連クエリを無効化(invalidate)して再取得する」方法です。更新結果がどの一覧・詳細・集計に影響するかを考え、該当するqueryKeyを狙って無効化します。これにより、次回参照時(または即時)に最新データが再フェッチされ、UIがサーバー状態へ収束します。

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

export function useUpdateTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateTodo,
    onSuccess: (updated) => {
      // 例)一覧と詳細の両方を最新化したいケース
      queryClient.invalidateQueries({ queryKey: ["todos"] });
      queryClient.invalidateQueries({ queryKey: ["todo", updated.id] });
    },
  });
}

この方針のメリットは、実装がシンプルで破綻しにくいことです。一方で、再取得が走るためネットワークコストや画面のチラつきが気になる場合があります。その場合は次の「キャッシュを書き換える」戦略と組み合わせます。

  • サーバー側で値が補正される(例:更新日時付与、権限でフィールドが変わる)なら、invalidateで再取得して確実に合わせる
  • 影響範囲が広い更新(例:一覧の並び替えや集計に影響)も、まずはinvalidateを優先すると安全

更新時にキャッシュを書き換える(楽観的更新の入口)

ユーザー体験を優先して「更新結果を待たずにUIを先に反映」したい場合、キャッシュを手動で更新します。react queryではsetQueryDataを使い、クエリキャッシュの内容を直接書き換えられます。ここでは“楽観的更新(Optimistic Update)”の入口として、まずは成功後にキャッシュを反映する形から押さえると安全です。

成功後に返ってきた更新済みデータで、詳細キャッシュを更新し、一覧の該当要素も差し替える例です。

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

type Todo = { id: string; title: string; completed: boolean };

export function useUpdateTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateTodo,
    onSuccess: (updated) => {
      // 詳細キャッシュを更新
      queryClient.setQueryData(["todo", updated.id], updated);

      // 一覧キャッシュが存在する場合のみ差し替え
      queryClient.setQueryData<Todo[]>(["todos"], (prev) => {
        if (!prev) return prev;
        return prev.map((t) => (t.id === updated.id ? updated : t));
      });
    },
  });
}

このアプローチは「再取得を待たずに画面が即座に追従する」点が強みですが、次の点を意識しないと整合性が崩れます。

  • キャッシュに存在しないクエリキーは更新できない(prevundefinedになり得る)
  • 一覧・詳細・フィルタ別一覧など、同一データの複数キャッシュがある場合は“更新漏れ”が起きやすい
  • サーバーが返す確定値(正)と、クライアントが仮に置いた値(仮)がズレると不整合になるため、必要に応じてinvalidateで最終同期を取る

“楽観的更新”を本格的に行う場合は、更新前にキャッシュを退避し、失敗時にロールバックする設計が必要です。まずは「成功後にsetQueryData」→「必要に応じてinvalidateで再同期」という順で段階的に導入すると、react queryの更新処理が安定します。

二重送信を防ぐ設計のポイント

更新系はユーザー操作(クリック/タップ/Enter連打)で二重送信が起きやすく、データ不整合やエラーの原因になります。react queryではミューテーションの状態を使って、UIと実行制御の両面から防止します。

  • 送信中はボタンを無効化するisPendingを見てUI操作をブロックする
  • イベントハンドラ側で多重起動をガードする:送信中は早期returnする
  • 同一入力の連続送信を避ける:送信ボタンを押した瞬間にフォームをロックし、成功/失敗で解除する
function TodoEdit({ id, initialTitle }: { id: string; initialTitle: string }) {
  const [title, setTitle] = useState(initialTitle);
  const updateTodoMutation = useUpdateTodo();

  const onSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (updateTodoMutation.isPending) return; // 多重送信ガード

    await updateTodoMutation.mutateAsync({ id, title });
  };

  return (
    <form onSubmit={onSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        disabled={updateTodoMutation.isPending}
      />

      <button type="submit" disabled={updateTodoMutation.isPending}>
        更新
      </button>

      {updateTodoMutation.isError && (
        <p>更新に失敗しました。</p>
      )}
    </form>
  );
}

二重送信対策は「フロントだけで完全に防ぐ」のは難しいため、少なくともUI上の連打を抑止し、同時実行を起こしにくい導線にするのが現実的です。react queryのisPendingを中心に据えると、実装が散らばらず保守しやすくなります。

QueryClientでよく使う操作メソッド

ocean+view

React Query(TanStack Query)では、コンポーネント側のuseQueryuseMutationだけでなく、中央の司令塔であるQueryClientからキャッシュや再取得を直接コントロールできます。特に「更新後にどのデータをいつ取り直すか」「一時的にキャッシュを書き換えるか」「不要なキャッシュを消すか」といった運用面で、QueryClientの操作メソッドは必須です。

ここでは実務で頻出の4つ(invalidateQueries / refetchQueries / removeQueries / setQueryData)に絞って、用途・挙動・使い分けの勘所を整理します。

invalidateQueries(無効化)

invalidateQueriesは「該当クエリのキャッシュを古い(stale)扱いにする」操作です。データそのものを即座に消すのではなく、「次に必要になったら取り直すべき」というフラグを立てるイメージで、React Query運用の基本になります。

典型例は、更新系処理(作成・編集・削除)の後に、関連する一覧や詳細を“正しい状態に戻す”ための無効化です。無効化対象はqueryKeyで指定し、前方一致(プレフィックス)でまとめて対象にすることもできます。

// 例:ユーザー一覧に関するクエリをまとめて無効化
queryClient.invalidateQueries({ queryKey: ['users'] });

// 例:特定IDの詳細だけ無効化
queryClient.invalidateQueries({ queryKey: ['users', userId] });

使いどころの判断軸は次の通りです。

  • 「すぐに画面更新しなくてもよいが、次回表示時には必ず新鮮であってほしい」ときはinvalidateQueriesが適します。
  • 無効化後、対象クエリが画面上で購読されている(アクティブ)場合は、設定や状況により自動的に再取得が走ることがあります。逆に非アクティブなら「次に使われるタイミング」まで取り直しを遅延できます。

注意点として、広すぎるキー(例:['users']を多用)で無効化すると、意図せず多数のクエリが再取得対象になり、通信量や負荷が増えることがあります。関連範囲を設計しつつ、必要な粒度でqueryKeyを切るのがコツです。

refetchQueries(再取得)

refetchQueriesは、指定したクエリを「今すぐ再取得する」ための操作です。invalidateQueriesが“古い扱いにする”のに対し、refetchQueriesは“即座に取りに行く”点が決定的に異なります。

たとえば「ユーザーが手動で更新ボタンを押した」「操作完了直後に最新値を必ず表示したい」といった、即時反映が求められる場面で有効です。

// 例:現在の画面で表示しているユーザー一覧を即再取得したい
await queryClient.refetchQueries({ queryKey: ['users'] });

// 例:特定の詳細を即再取得
await queryClient.refetchQueries({ queryKey: ['users', userId] });

使い分けのポイントは次の通りです。

  • 「いま表示している情報を確実に最新化したい」ならrefetchQueriesが分かりやすい選択です。
  • 一方で、即時リフェッチは通信タイミングを強制するため、多用すると体感速度やAPI負荷に影響します。更新後はまずinvalidateQueriesで十分か、要件に合わせて検討します。

removeQueries(削除)

removeQueriesは、指定したクエリをキャッシュから「削除」します。無効化(stale化)とは違い、キャッシュそのものがなくなるため、次回参照時は“初回取得”に近い挙動になります。

典型的には「ログアウト時にユーザー情報や権限情報を消したい」「検索条件が大幅に変わり、過去条件のキャッシュを持ち続ける意味が薄い」といった、状態のリセットや情報漏えい対策の文脈で使われます。

// 例:ログアウト時に、ユーザー関連のキャッシュをまとめて削除
queryClient.removeQueries({ queryKey: ['users'] });

// 例:特定の詳細だけ削除
queryClient.removeQueries({ queryKey: ['users', userId] });

運用上の注意点は次の通りです。

  • 削除は“戻せない”操作なので、単に最新化したいだけならinvalidateQueriesの方が安全です。
  • 削除後はキャッシュがないため、画面遷移などで同じクエリが必要になった際、再度ネットワーク取得が発生します(再表示が多い画面では体感に影響しやすい)。

setQueryData(手動更新)

setQueryDataは、React Queryのキャッシュを「手動で書き換える」ためのメソッドです。サーバーからの応答を待たずに見た目を更新したいときや、既知の変更が局所的で再取得するほどでもないときに強力です。

更新対象はqueryKeyで指定し、第二引数に新しいデータ、または旧データから更新する関数を渡します。関数形式にしておくと、既存キャッシュがある場合に安全に差分更新できます。

// 例:ユーザー詳細キャッシュを手動で更新(関数形式)
queryClient.setQueryData(['users', userId], (prev) => {
  if (!prev) return prev;
  return { ...prev, name: 'New Name' };
});

一覧の一部だけを差し替えるようなケースでも、同様に“配列の該当要素だけ更新”が可能です。

// 例:ユーザー一覧の中の該当ユーザーだけ更新
queryClient.setQueryData(['users'], (prev) => {
  if (!prev) return prev;
  return prev.map((u) => (u.id === userId ? { ...u, name: 'New Name' } : u));
});

使いどころの目安は次の通りです。

  • 再取得(refetch)を減らし、UIの即時性を上げたいときに有効です。
  • 更新後の整合性が不安な場合は、手動更新に加えてinvalidateQueriesで保険をかける運用もあります(ただし、目的が「通信削減」なら無効化の範囲やタイミングを慎重に設計します)。
  • キャッシュの形(スキーマ)を誤って書き換えると、画面側の前提が崩れて不具合になりやすいので、更新ロジックは最小限・局所的に保つのが安全です。

プロジェクトでの運用ルール(開発方針)

react+query+cache

React Query(TanStack Query)は柔軟性が高い一方で、チーム開発では「人によって設定や書き方が違う」状態になりやすいライブラリです。結果として、画面ごとに再取得頻度がバラついたり、エラー処理が散らばったり、queryKeyが統一されずキャッシュが期待通りに効かない、といった運用上のコストが増えます。

このセクションでは、プロジェクトでReact Queryを安定運用するために、全体デフォルト・命名規約・エラーハンドリングをどう設計し、チームの開発方針として固定するかを整理します。

デフォルトオプションの設計(全体)

まず重要なのは「個別のuseQuery/useMutationで毎回頑張らない」ことです。React QueryはQueryClientのデフォルトオプションで、全体方針(再試行、再取得、キャッシュ保持など)を強制できます。

設計の考え方はシンプルで、プロダクト全体で“だいたい正しい”標準値を決め、例外が必要な画面だけ個別指定します。これにより、パフォーマンス・通信量・UXの期待値をチームで揃えられます。

クエリのデフォルト設定(staleTime等)

クエリ(useQuery系)では、特に以下の項目が運用に直結します。

  • staleTime:データを「新鮮」とみなす時間。短いほど頻繁に再取得され、長いほどキャッシュが活きます。
  • gcTime(旧cacheTime相当):参照がなくなった後にキャッシュが破棄されるまでの時間。画面遷移の多いアプリでは体感速度に影響します。
  • refetchOnWindowFocus:フォーカス復帰時の自動再取得。ダッシュボード等では有益ですが、入力フォームでは体験を悪化させる場合があります。
  • refetchOnReconnect:ネットワーク復帰時の再取得。モバイル回線の不安定さを考えると有効な場面が多いです。
  • retry / retryDelay:失敗時の自動リトライ。サーバ負荷・認証エラー時の挙動とセットで設計すべきです。

運用ルールとしては、たとえば「一覧系は短めのstaleTime、マスタ参照系は長め」などの基準を文章化し、画面ごとの思いつきでstaleTimeを乱立させないようにします。迷ったら“標準値”に寄せ、数値を変えるときは理由(更新頻度、正確性要件、通信制約)をコードコメントや設計メモに残すのが効果的です。

ミューテーションのデフォルト設定

ミューテーション(useMutation系)は、クエリ以上に「失敗時の扱い」「多重実行」「UIへの通知」がチームで散らばりやすい領域です。デフォルトとして決めておくと運用が安定します。

  • retry:ミューテーションのリトライは慎重に。非冪等(同じリクエストを再送すると副作用が重なる)なAPIでは、安易な自動リトライが事故につながります。
  • onError:通知方法(トースト、ダイアログ、フォーム項目のエラー表示)を共通化する入口。ここを各画面に散らすと統一感が崩れます。
  • onSuccess / onSettled:成功後のキャッシュ更新や無効化(invalidate)をどこで行うかの方針を固定します。

特に重要なのは、「ミューテーションは基本retryしない(あるいは限定的にする)」という方針をまず検討することです。ネットワーク瞬断に強くしたい場合でも、APIが冪等かどうか、サーバ側で重複排除できるか、を確認した上で段階的に許可するのが安全です。

デフォルト設定の具体例

以下は「チームの基準点」を作るためのQueryClient設定例です。実際の値はプロダクトの特性(更新頻度、リアルタイム性、通信環境)に合わせて調整してください。重要なのは“個別設定の前に標準を置く”ことです。

import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 標準の鮮度。画面要件が明確な場合のみ個別に上書きする
      staleTime: 30 * 1000, // 30s
      // 参照が外れた後もしばらく残して、戻る操作を高速化
      gcTime: 10 * 60 * 1000, // 10m

      // 自動再取得はプロダクト方針で統一
      refetchOnWindowFocus: false,
      refetchOnReconnect: true,

      // リトライは認証エラー等と衝突しないように上限を抑える
      retry: 1,
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10_000),
    },

    mutations: {
      // 非冪等な更新を想定し、原則リトライしない方針に寄せる
      retry: 0,
    },
  },
})

この“標準”に対して、たとえば「ほぼ更新されないマスタ参照はstaleTimeを長くする」「入力フォーム画面はフォーカスリフェッチを無効化する」といった例外だけをローカルに上書きする運用にすると、React Queryの設定が読みやすくなり、レビューもしやすくなります。

クエリキー命名規約の決め方

React QueryのキャッシュはqueryKeyがすべての基準になります。命名が揃っていないと、同じデータのはずなのに別キー扱いになってキャッシュが分断され、invalidateの漏れや二重取得の原因になります。

命名規約は「何となく」で済ませず、以下をルール化しておくと堅牢です。

  • 配列キーを前提にする:文字列単体ではなく['resource', params]形式を標準にし、構造で意味が分かるようにする。
  • 先頭はリソース(ドメイン)で固定:例:['users', ...]['projects', ...]のように、API/エンティティ単位のトップレベルを揃える。
  • 詳細・一覧・検索でセグメントを分ける:例:一覧は['users', 'list', { page }]、詳細は['users', 'detail', userId]
  • パラメータはオブジェクトで渡し、キー順を安定させる:同値なのに別オブジェクト生成で差分が出ないよう、共通の生成関数に寄せる。
  • queryKey生成を関数化して集約する:各所で手書きせず、queryKeysのような定義ファイルに統一する。

特に「生成関数の集約」は効果が大きいです。これにより、命名の揺れを防ぎつつ、invalidate対象をレビューしやすくなります。

// queryKeys.ts
export const queryKeys = {
  users: {
    all: ['users'] as const,
    list: (params: { page: number; q?: string }) => ['users', 'list', params] as const,
    detail: (userId: string) => ['users', 'detail', userId] as const,
  },
}

運用上の注意として、「invalidateの都合で曖昧なキーに寄せる」のではなく、まず意味のある粒度でキーを設計し、その上で必要な範囲をinvalidateできるように階層(['users']をルートにする等)を整える、という順序が失敗しにくいです。

エラーハンドリングの共通化

React Queryを導入すると、エラーは各useQuery/useMutationのerrorに出てきます。しかし、画面ごとに処理がバラバラだと、同じエラーなのにメッセージが違う・ログが欠ける・復旧導線がない、といった問題が起きます。

共通化の基本方針は次の3点です。

  • ユーザー表示:想定されるエラーは分かりやすい文言へ、想定外は汎用文言へ寄せる(詳細はログへ)。
  • 開発者向けログ/計測:どのqueryKeyで失敗したか、リクエスト条件、HTTPステータス等を揃えて追えるようにする。
  • 認証・権限・メンテ等の“横断ケース”:401/403/503などは画面固有ではなくアプリ共通の復旧を用意する。

実装としては、HTTPクライアント層(fetch/axiosラッパ)でエラー型を正規化し、React Query側ではonErrorやError Boundary、あるいは共通ハンドラを通して扱う、という分離が運用しやすくなります。

401発生時の再試行・再認証の考え方

401(Unauthorized)が発生したときの扱いは、React Query運用で最も事故が起きやすいポイントの一つです。理由は、retryと組み合わさると、「401をリトライし続ける」「再認証処理が多重に走る」などの不具合に直結するためです。

運用ルールとして、まず次を切り分けます。

  • アクセストークン期限切れ(再認証/更新で復旧可能):トークン更新(リフレッシュ)→成功したら元リクエストを再実行、という流れをアプリ共通で持つ。
  • ログアウト状態・無効トークン(復旧不可):ログイン画面へ誘導、セッション破棄、ユーザーに再ログインを促す。

次に、React Queryのリトライ方針としては、少なくとも401に対して機械的なretryをしないルールを推奨します。再認証フローが必要な場合は、HTTPクライアント側で「401を受けたら一度だけトークン更新を試す」「同時多発を1回にまとめる(単一フライト)」などの制御を行い、その結果として成功したリクエストだけを再送します。

このときのチーム内の合意事項としては、以下を明文化しておくと運用が崩れにくくなります。

  • 401時にReact Queryのretryに任せない(任せる場合でも例外条件を明確化する)
  • 再認証はアプリ全体で単一の仕組みに寄せ、画面ごとに実装しない
  • 再認証に失敗した場合の遷移・表示(ログイン誘導、メッセージ)を統一する
  • 再認証中の並行リクエストの扱い(待たせる/失敗させる)を決める

これらを「開発方針」として固定しておくことで、React Queryの強み(キャッシュと同期)を活かしながらも、認証まわりの不安定さを最小化できます。

ページネーション・無限スクロールへの対応

react+query+pagination

一覧画面のデータ取得は、件数が増えるほど「どう分割して読み込むか」が重要になります。react query(TanStack Query)では、ページ番号で切り替えるページネーションと、スクロールに応じて追加取得する無限スクロールを、それぞれ適したフックで実装できます。ここでは実装方針を整理し、UIの体験とキャッシュの扱いを両立するための考え方をまとめます。

ページ送りの実装方針

ページ送り(例:1ページ目、2ページ目…)は、page を状態として持ち、queryKey にページ番号(やページサイズ、フィルタ条件)を含めて取得するのが基本方針です。これにより、react query のキャッシュが「ページ単位」で分離され、戻る操作でも即座に表示しやすくなります。

実装の要点は次のとおりです。

  • queryKeyにページ情報を含める['items', { page, pageSize, sort }] のように、同じ一覧でも条件が違えば別キャッシュになる形にする
  • ページ切り替え時のチラつきを抑えるplaceholderData(または移行用途の keepPreviousData)で前ページの表示を維持し、取得完了後に自然に差し替える
  • 「次ページがあるか」の判断をAPIレスポンスに寄せるtotalhasNextnextPage など、UIが判断できる情報を返す設計にする

ページングの代表的なコード例です(ページ番号ベース)。

import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'

type Item = { id: string; name: string }
type PageResponse = { items: Item[]; total: number }

async function fetchItems(params: { page: number; pageSize: number }): Promise<PageResponse> {
  const res = await fetch(`/api/items?page=${params.page}&pageSize=${params.pageSize}`)
  if (!res.ok) throw new Error('Failed to fetch')
  return res.json()
}

export function ItemsPagination() {
  const [page, setPage] = useState(1)
  const pageSize = 20

  const query = useQuery({
    queryKey: ['items', { page, pageSize }],
    queryFn: () => fetchItems({ page, pageSize }),
    // ページ切り替え時に前データを一時表示してチラつきを軽減
    placeholderData: (prev) => prev,
  })

  const items = query.data?.items ?? []
  const total = query.data?.total ?? 0
  const maxPage = Math.max(1, Math.ceil(total / pageSize))

  return (
    <div>
      {query.isPending && <p>Loading...</p>}
      {query.isError && <p>Error</p>}

      <ul>
        {items.map((it) => (<li key={it.id}>{it.name}</li>))}
      </ul>

      <button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1}>
        Prev
      </button>
      <span> {page} / {maxPage} </span>
      <button onClick={() => setPage((p) => Math.min(maxPage, p + 1))} disabled={page === maxPage}>
        Next
      </button>
    </div>
  )
}

また、ページ送りのUXを上げるなら「次ページを先読みしておく」方針も有効です。例えば「Next」ボタンが押されやすい導線なら、現在ページ表示中に次ページのクエリを温めておくことで、遷移時の待ち時間を減らせます。先読みは QueryClient のプリフェッチで実現でき、設計としては「ユーザーが次に到達しそうなページのみ」を対象にするのが過剰取得を避けるコツです。

無限スクロールの実装方針

無限スクロール(Infinite Scroll)は、「次のページ」を順次取得して配列を連結していくため、react query の useInfiniteQuery を使う方針が適しています。ポイントは、ページ番号よりもカーソル(次取得位置)を使う設計に寄せると、データの追加・削除が起きるAPIでもズレが起きにくく、安定した無限スクロールになります。

実装方針の中核は次の3点です。

  • クエリ関数は「pageParam」を受け取るuseInfiniteQuery が渡す pageParam を次ページ取得のパラメータに使う
  • getNextPageParamで次カーソルを返す:レスポンスの nextCursor(なければ undefined)を返し、続きがあるかをreact query側に判断させる
  • 表示はpagesをフラット化するdata.pagesflatMap 等で結合してレンダリングする

カーソルベースの例です。

import { useInfiniteQuery } from '@tanstack/react-query'

type Item = { id: string; name: string }
type CursorResponse = { items: Item[]; nextCursor?: string }

async function fetchItemsByCursor(params: { cursor?: string; limit: number }): Promise<CursorResponse> {
  const url = new URL('/api/items', window.location.origin)
  if (params.cursor) url.searchParams.set('cursor', params.cursor)
  url.searchParams.set('limit', String(params.limit))

  const res = await fetch(url.toString())
  if (!res.ok) throw new Error('Failed to fetch')
  return res.json()
}

export function ItemsInfinite() {
  const limit = 20

  const query = useInfiniteQuery({
    queryKey: ['items', { limit }],
    queryFn: ({ pageParam }) => fetchItemsByCursor({ cursor: pageParam, limit }),
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  })

  const items = query.data?.pages.flatMap((p) => p.items) ?? []

  return (
    <div>
      <ul>
        {items.map((it) => (<li key={it.id}>{it.name}</li>))}
      </ul>

      {query.isFetchingNextPage && <p>Loading more...</p>}

      <button
        onClick={() => query.fetchNextPage()}
        disabled={!query.hasNextPage || query.isFetchingNextPage}
      >
        Load more
      </button>
    </div>
  )
}

無限スクロールを「スクロール到達で自動追加」にする場合は、末尾の監視(Intersection Observerなど)で fetchNextPage() を呼ぶのが一般的です。その際の方針としては、次を守ると実運用で安定します。

  • 二重取得を防ぐ:監視トリガー側で isFetchingNextPage を見て連続呼び出しを避ける
  • 「もうない」状態を明示するhasNextPage が false のときは監視を止める/「これ以上ありません」を出す
  • 条件変更時は別クエリとして扱う:検索条件やソートが変わるなら queryKey に含め、結果の混線(前条件の続きを取ってしまう)を防ぐ

ページ送りは「任意ページへのジャンプ」、無限スクロールは「連続的な探索」と相性が良い実装です。react query を前提にするなら、ページ送りは useQuery+ページを含む queryKey、無限スクロールは useInfiniteQuery+カーソル設計、という方針に分けると実装も運用も整理しやすくなります。

パフォーマンス最適化(レンダリングとデータ参照)

react+query+performance

React Query(TanStack Query)はキャッシュと購読の仕組みにより、データ取得そのものだけでなく「どのタイミングで・どのコンポーネントが再レンダリングされるか」によって体感性能が大きく変わります。特に一覧画面やダッシュボードのように表示要素が多いUIでは、わずかな参照の揺れや購読範囲の広さが、不要な再レンダリングを増やす原因になります。

ここでは、レンダリングとデータ参照に焦点を当てて、React Queryを使ったときに効きやすい最適化の考え方を整理します。

不要なオブジェクト生成を避ける(参照の安定化)

Reactでは「値が同じ」でも「参照が違う」だけでprops比較に失敗し、子コンポーネントが再レンダリングされることがあります。React Query利用時も同様で、useQuery周辺で毎回新しいオブジェクトや関数を生成していると、UI側が無駄に更新されやすくなります。

代表例は、レンダリングごとに生成されるオプションオブジェクトやコールバック、そしてクエリ結果から派生した「整形済みデータ」です。次のような「毎回新しい参照」を作る書き方は、コンポーネント分割やReact.memoと組み合わさったときに効いてきます。

// 悪い例:毎回optionsが新しくなる(参照が不安定)
const query = useQuery({
  queryKey: ['users', orgId],
  queryFn: () => fetchUsers(orgId),
  select: (data) => data.items.map(u => ({ id: u.id, name: u.name })), // 毎回新配列/新オブジェクト
})

避けたいのは「必要以上に新しい参照を生成して、結果的に再レンダリングを誘発する」状態です。対策としては、次の観点で参照を安定させます。

  • オプションやコールバックを毎回生成しない:依存が変わらないならuseMemo/useCallbackで安定化させ、子に渡すpropsの参照が揺れないようにします。

  • 整形は「必要な箇所で」「必要最小限」にする:クエリ結果を大量にmapして新オブジェクトを作ると、更新がなくても参照が変わりやすくなります。派生データの生成コストと再レンダリングの影響範囲を意識します。

  • データ構造を安定させる:同じ内容でも都度別オブジェクトに組み替えるのではなく、UIが本当に必要とする形に寄せつつ、更新があるときだけ変わる設計を目指します。

ポイントは「React Queryが返すデータ」そのものよりも、その周辺で作っているオブジェクト参照が再レンダリングのトリガーになり得る、という点です。

レンダリング中の参照設計を見直す

パフォーマンスを落としやすいのは、「レンダリング中に重い処理を行う」ことと、「参照の揺れが広範囲に伝播する」ことです。React Queryの導入後に画面が重くなった場合、ネットワークではなくレンダリング側に原因があるケースも珍しくありません。

見直すべき典型パターンを挙げます。

  • クエリ結果を上位コンポーネントで一括加工し、子へ丸ごと配る

    上位でdataを大きく整形すると、その親が再レンダリングされるたびに加工結果の参照が変わり、子コンポーネントが連鎖的に更新されます。重い加工は「購読を絞ったコンポーネント」側に寄せる、またはメモ化して影響範囲を限定します。

  • 「必要な1フィールドのために巨大オブジェクトを参照」している

    例えばヘッダーがユーザー名だけ必要なのに、ユーザー全体(設定・権限・大量のメタ情報)を参照していると、どこかのフィールド更新でヘッダーまで再レンダリングされます。参照の粒度を揃える設計が重要です。

  • リスト描画で、要素ごとに参照が毎回変わる

    リストの各行に{...item}のような新オブジェクトを渡す、行内で毎回関数を生成して渡す、といったことが積み重なると、行数に比例してコストが増えます。行コンポーネントのpropsは「安定参照+必要最小限」を意識します。

つまり、React Queryのデータを「どこで購読し」「どの単位で加工し」「どの粒度で子に渡すか」という参照設計が、レンダリング性能を左右します。データ取得の成功/失敗などの状態変化が起きたときに、どのコンポーネントまで巻き込まれるかを、コンポーネントツリーで説明できる形にしておくのが理想です。

selectorで必要なデータだけを購読する

React Queryの最適化で特に効果が出やすいのが、select(いわゆるselector)を使って「必要なデータだけを購読する」ことです。コンポーネントが必要とする形にデータを変換・抽出しておくことで、関心のない変更による再レンダリングを減らしやすくなります。

例えば、ユーザー一覧のうち「表示に必要なフィールドだけ」を購読する場合は次のようにします。

const { data: userRows } = useQuery({
  queryKey: ['users', orgId],
  queryFn: () => fetchUsers(orgId),
  select: (data) => data.items.map((u) => ({
    id: u.id,
    name: u.name,
    role: u.role,
  })),
})

この設計の意図は、UIが使わないフィールド(例:詳細設定やメタ情報)に変更があっても、行描画が巻き込まれにくい状態を作ることです。加えて、selectorを使うと「表示用データ」の責務をuseQuery側に寄せられるため、コンポーネントのレンダリング中に都度加工するよりも、見通しが良くなることがあります。

一方で、selectorは万能ではなく、使い方を誤ると逆効果にもなります。次の点を押さえると安全です。

  • select内で重すぎる加工をしない:大規模な並べ替え・集計・ネスト変換などはコストが高くなりがちです。必要なら加工単位を分け、影響範囲を小さくします。

  • 「必要最小限の抽出」に留める:UIが使うフィールドだけに絞ると、購読範囲が明確になります。逆に何でも詰めると最適化の意味が薄れます。

  • 参照の変化がレンダリングに与える影響を意識する:selectが毎回新しい配列・オブジェクトを返す以上、子コンポーネント側のprops設計(安定化)とセットで考えると効果が出やすくなります。

React Queryのselectorを適切に使うと、「データ取得」ではなく「再レンダリングの抑制」という観点で、体感速度の改善につながります。特に、複数コンポーネントが同じクエリを参照している画面では、購読データを絞る価値が大きいです。

内部構造をざっくり理解して設計に活かす

react+query+cache

React Query(TanStack Query)は「使える」だけでも十分便利ですが、内部構造をざっくり掴むと、キャッシュ戦略・再レンダリング範囲・責務分離の判断が一段ラクになります。ここでは実装詳細に踏み込みすぎず、「どの部品が何を担当しているか」を設計に活かせる粒度で整理します。

QueryClientの役割

QueryClientは、React Queryの“司令塔”です。アプリ全体のクエリ状態やキャッシュ操作の窓口になり、コンポーネントは原則としてQueryClientを直接触らず、useQueryなどのフック経由で恩恵を受けます。

設計観点で重要なのは、QueryClientが「グローバルなデータ取得・キャッシュのルール」を集約する点です。たとえば、どのクエリも共通で従うべき方針(再試行、フォーカス復帰時の挙動、エラーログの取り方等)をQueryClientに寄せることで、画面ごとの実装ブレを減らせます。

  • アプリ内のクエリ/ミューテーションの設定・操作の中心
  • キャッシュ(QueryCache)へのアクセス窓口
  • 「各画面での場当たり対応」ではなく「全体最適のルール」を置く場所

キャッシュを保持する領域の考え方

React Queryのキャッシュは「コンポーネントの状態」ではなく、「クライアント(QueryClient)配下のストア」に保持されます。つまり、画面がアンマウントしても、キャッシュ自体はすぐには消えず、設定された条件に従って生存します。

この“コンポーネント外にあるキャッシュ領域”という前提を持つと、設計が整理しやすくなります。具体的には、データ取得の責務をUIから切り離し、「同一データは同一キーで共有する」「表示側は必要な部分だけ購読する」といった方針が立てやすくなります。

  • キャッシュはUIツリーに紐づかない(アンマウント=即消滅ではない)
  • 同一のqueryKeyなら同一キャッシュを参照できる
  • 「画面ごとのuseState」ではなく「共有キャッシュ」を前提に設計できる

QueryCacheの役割

QueryCacheは、クエリの集合を保持するレジストリのような存在です。各クエリ(後述)がどれだけ存在しているか、どのキーのクエリがあるか、といった“保管庫”を担います。

設計においては、「クエリはQueryCacheに登録され、キーで一意に管理される」という理解が重要です。これにより、コンポーネントが増えても、同じqueryKeyを使っている限りはキャッシュは共有され、重複取得や状態の分裂を避けやすくなります。

  • クエリ(Query)をキー単位で保持・参照する場所
  • 同じキーのクエリは“同じ入れ物”を共有するという前提を作る
  • キャッシュの全体像を束ねるレイヤーとして理解すると設計が安定する

Query(単位データ)の概念

Queryは、React Queryにおける“キャッシュの最小単位”です。ざっくり言えば「queryKeyで識別され、取得結果・取得状態・更新時刻などを持つ1つのデータ箱」です。

ここでのポイントは、Queryが単にデータ(レスポンス)だけでなく、データ取得に付随するメタ情報(成功/失敗、エラー、フェッチ中か、いつ更新されたか等)も合わせて抱える点です。そのため、UIは「データがあるか」だけではなく、「いま何が起きているか」をQueryの状態から一貫して表現できます。

  • 単位は“APIエンドポイント”ではなく“キーで定義されるデータ”
  • データ本体+取得状態(status/error/fetchingなど)を内包する
  • 同じキー=同じQueryを共有し、画面間の整合性を取りやすい

QueryObserver(購読/再レンダリング)の仕組み

QueryObserverは、コンポーネント(正確にはフック)とQueryの間に入る“購読者”です。useQueryを呼ぶと、内部的にはQueryObserverがQueryを監視し、状態変化に応じてコンポーネントへ更新通知を出します。

この理解は、レンダリング設計に直結します。つまり、React Queryは「Queryの状態が変わったら、購読している部分だけを更新する」ことを狙っており、無闇な再レンダリングを避けるための仕組みが分離されています。複数コンポーネントが同じQueryを参照しても、それぞれのObserverが購読し、必要に応じて個別に再レンダリングが走る、という見立てになります。

  • useQueryはQueryを直接読むのではなくObserver経由で購読する
  • Queryの変化を検知し、Reactに更新を伝える橋渡し役
  • 同一Queryに複数Observerがぶら下がる構造を想定すると設計しやすい

アクティブ/非アクティブなクエリの違い

クエリには「アクティブ(active)」と「非アクティブ(inactive)」の状態があります。直感的には、アクティブは“誰かが購読している(画面上で使われている)”、非アクティブは“誰も購読していない(いま表示には使われていない)”状態です。

この区別を押さえると、画面遷移が多いアプリでもキャッシュがどのように生き残り、どのタイミングで“見えないところで再取得される/されない”かの判断がしやすくなります。設計上は、アクティブなクエリはUIの要求に近く、非アクティブなクエリは「再利用に備えて残っているストック」と捉えると理解が進みます。

  • アクティブ:Observerが1つ以上存在し、UIに紐づいている
  • 非アクティブ:購読者がいないが、キャッシュとしては残り得る
  • 画面遷移時の“即再取得の有無”や“キャッシュ再利用”の説明がしやすくなる

全体アーキテクチャの俯瞰

ここまでをまとめると、React Queryの内部は「管理(QueryClient)」「保管(QueryCache/Query)」「通知(QueryObserver)」に大別できます。これを頭に置くと、設計時に“いま触っているのはどの層か”が明確になり、責務が混ざりにくくなります。

  • QueryClient:全体の管理・操作の起点
  • QueryCache:クエリの集合を保持する保管庫
  • Query:キー単位のデータ+状態の最小単位
  • QueryObserver:UIとQueryをつなぐ購読・通知機構

コンポーネント側から見たデータフロー

コンポーネントから見ると、React Queryのデータフローは「フック呼び出し → 購読開始 → キャッシュ参照/フェッチ → 状態変化で再レンダリング」という一本道に整理できます。重要なのは、コンポーネントが“データの置き場所”を持たず、Observer経由でQueryの状態を参照し続ける点です。

  1. コンポーネントでuseQueryを呼ぶ
  2. 内部でQueryObserverが作られ、queryKeyに対応するQueryを購読する
  3. QueryCacheに該当Queryがあればそれを参照し、なければ新規作成される
  4. Queryがキャッシュを返す/必要ならフェッチを進め、状態が更新される
  5. 更新をObserverが受け取り、購読中コンポーネントが再レンダリングする

この流れを前提にすると、設計の勘所は「キーの設計=Queryの単位設計」「どのコンポーネントが購読者になるか=再レンダリング境界の設計」の2点に集約されます。React Queryを単なる取得ライブラリとしてではなく、キャッシュと購読を中核にしたアーキテクチャとして扱えるようになります。

OpenAPIと組み合わせた型安全なAPIクライアント運用

ocean+view

React Query(TanStack Query)を実務で運用していくと、「APIの変更にフロントが追従できず壊れる」「エンドポイントごとの型定義が散らかる」「クエリ関数と型の整合性を人力で担保するのがつらい」といった課題に直面しがちです。そこで有効なのが、OpenAPI(Swagger)をソースとして型定義・フェッチ・クエリを一貫して生成し、型安全なAPIクライアント運用へ寄せるアプローチです。

ここでは openapi-typescriptopenapi-fetchopenapi-react-query の順に、OpenAPIからReact Queryで使うための“型安全な呼び出し”を組み立てる方法を整理します。

openapi-typescriptで型定義を生成する

openapi-typescript は、OpenAPI定義(JSON/YAML)から TypeScript の型定義(paths など)を自動生成するツールです。React Query単体でも型は書けますが、APIスキーマを単一の正として型を生成すると、API変更時の差分がコンパイルエラーとして表面化し、修正漏れを減らせます。

生成物は一般に「エンドポイントごとのリクエスト/レスポンス型」を含みます。例えば /users/{id}GET なら、パスパラメータやレスポンスの形が型として得られるため、後段のフェッチやReact Queryの戻り値まで自然に型が通ります。

代表的な生成コマンド例は以下です(OpenAPIの配置パスはプロジェクトに合わせて調整してください)。

# OpenAPIのスキーマから型定義を生成(例)
npx openapi-typescript ./openapi.yaml -o ./src/generated/openapi.d.ts

運用上は、次のような方針にしておくと破綻しにくいです。

  • 生成物は src/generated 等に集約し、手書きの型と混在させない
  • CIで生成差分を検知し、スキーマ変更に追従できているかを担保する
  • OpenAPI側の品質(レスポンス定義やエラー定義)を整えるほどフロントの型安全性が上がる

openapi-fetchで型安全にフェッチする

openapi-fetch は、先ほど生成した paths 型などを入力として、エンドポイント・HTTPメソッド・パラメータ・ボディ・レスポンスを型安全に扱えるfetchクライアントを提供します。これにより、React Queryに渡す「クエリ関数」を“型が崩れない形”で実装できます。

典型的には、生成済み型を読み込み、ベースURLなどを設定してクライアントを作ります。

import createClient from "openapi-fetch";
import type { paths } from "../generated/openapi"; // openapi-typescriptの生成型

export const api = createClient<paths>({
  baseUrl: "https://api.example.com",
});

呼び出し側は、GET/POST などのメソッドに対して、パスとオプション(params/body等)を渡します。OpenAPIの定義に沿わないパラメータを渡すと型エラーになり、レスポンスも定義通りの型になります。

// 例:GET /users/{id}
const { data, error } = await api.GET("/users/{id}", {
  params: { path: { id: "123" } },
});

// data の型は OpenAPIのレスポンス定義に基づく

React Queryの観点では、queryFn の内部でこの api.GET 等を呼ぶだけで、取得データの型が自然に推論されます。「Axios + 手書き型」のように型と実データのズレが起きにくくなる点がメリットです。

なお、認証ヘッダの付与や共通エラーハンドリングなどは、openapi-fetch側の設定(ミドルウェア/フック相当)でまとめておくと、各クエリ実装が薄くなり運用が楽になります。

openapi-react-queryでクエリを自動生成する

openapi-react-query は、OpenAPIスキーマ(および openapi-fetch のクライアント)と連携し、React Query向けのクエリ/ミューテーションを“型付きで”扱いやすくする仕組みを提供します。目的は、エンドポイントごとの queryKey 設計や queryFn のボイラープレートを減らし、React Query運用を標準化することです。

基本イメージとしては、

  • エンドポイントとパラメータから一貫したキーを作る
  • 内部で openapi-fetch を呼び出す
  • レスポンス型がOpenAPIから自動で推論される

といった流れを、毎回手で書かずに済むようにします。これにより、画面ごとに実装者が異なっても、React Queryの使い方(型・キー・呼び出し)が揃いやすくなります。

また、OpenAPIベースでクエリが組み立てられると、APIの追加・変更が発生した際に「スキーマ更新 → 生成更新 → コンパイルエラーを直す」という形で追従でき、保守性が上がります。React Queryのキャッシュ活用はそのままに、クライアント層の型安全性を底上げできるのがポイントです。

導入手順(OpenAPI連携のセットアップ)

OpenAPI連携は、生成物と実行時クライアントを段階的に導入するとスムーズです。代表的なセットアップ手順を、プロジェクトに組み込みやすい形でまとめます。

  1. OpenAPIスキーマを用意する

    バックエンドが提供する openapi.json/openapi.yaml

  2. openapi-typescript で型定義を生成する

    生成先を固定し、手書きコードと分離します。

    npx openapi-typescript ./openapi.yaml -o ./src/generated/openapi.d.ts
  3. openapi-fetch のクライアントを用意する

    ベースURLや共通ヘッダ付与(認証など)を集約したモジュールを作ります。

  4. openapi-react-query を接続する

    プロジェクトの方針に合わせ、生成/ラッパーを導入してReact Queryから呼べる形を整えます。

  5. 生成を自動化する(推奨)

    package.json のスクリプト化やCIでのチェックを入れ、スキーマ変更の追従漏れを減らします。

重要なのは、生成物を手で編集しない運用を徹底することです。生成に手を入れると再生成で消えるため、拡張が必要ならラッパー層(自前の関数)を別ファイルで用意します。

基本的な使い方(生成クライアントの利用)

セットアップ後は、画面側(コンポーネント/フック)で「生成された型安全なクライアント」を使ってReact Queryのクエリを組み立てます。ここでは、考え方を掴むために最小の利用イメージを示します。

まず、openapi-fetch クライアントをクエリ関数から呼び出し、成功時のデータだけを返す形にすると、React Query側が扱いやすくなります。

import { useQuery } from "@tanstack/react-query";
import { api } from "../lib/apiClient"; // createClient<paths>したもの

export function useUser(id: string) {
  return useQuery({
    queryKey: ["users", id],
    queryFn: async () => {
      const { data, error } = await api.GET("/users/{id}", {
        params: { path: { id } },
      });
      if (error) throw error;
      return data;
    },
  });
}

この形にしておくと、OpenAPI由来の型が data に乗り、呼び出し側は型安全に利用できます。

openapi-react-query を併用する場合は、同じ目的(型安全な取得)をより少ない定型コードで実現しやすくなります。プロジェクト内で「OpenAPI連携の呼び出しはこの流儀で書く」と決めておくと、React Queryの運用が属人化しにくくなり、保守性が上がります。

比較観点で選ぶ(Axios/SWR/React Query)

ocean+view

フロントエンドのデータ取得・同期をどう設計するかは、プロダクトの速度感と保守性を左右します。ここでは「Axios」「SWR」「React Query(TanStack Query)」を、実務で差が出やすい比較観点(性能・開発体験・導入容易性・互換性)で整理します。結論として、HTTPクライアント(Axios)と“サーバー状態管理”(SWR/React Query)は役割が異なるため、同列比較ではなく「何を解決したいか」で選ぶのがポイントです。

性能(キャッシュ・再取得・バックグラウンド更新)

性能面での差は「キャッシュ戦略」と「再取得の自動化」に集約されます。Axiosは通信を行うためのライブラリで、キャッシュやバックグラウンド更新は基本的に自前実装です。一方でSWRとReact Queryは“取得したデータをどう保持し、いつ更新するか”までを含めて設計されています。

  • Axios:キャッシュは標準機能ではないため、ブラウザキャッシュ、CDN、Service Worker、独自ストア(Redux等)、もしくは別ライブラリで補完します。再取得のタイミング制御も手動になりやすく、画面数が増えるほど「どこでいつ再フェッチするか」が分散しがちです。

  • SWR:Stale-While-Revalidateの思想に沿って、表示はキャッシュ(stale)を返しつつ裏で再検証(revalidate)する動きが得意です。フォーカス復帰時の自動再取得など、軽量に“更新され続けるUI”を作れます。小〜中規模で「とりあえず良い感じに最新化したい」用途にハマりやすいです。

  • React Query:キャッシュ・再取得・バックグラウンド更新を細かく制御しやすく、画面数やデータ量が増えたときに強みが出ます。特に、クエリ単位でのキャッシュの扱い、必要に応じた再取得、更新後の整合性維持といった“データ同期の運用”に耐える設計です。結果として、同じAPIを複数画面で参照しても無駄な再取得を抑えやすく、体感パフォーマンスの改善に繋がります。

「一覧→詳細→戻る」のような遷移が多いUIでは、React Queryのキャッシュが効くと再表示が速くなりやすい一方、常に最新性が必要な領域では再取得ポリシーの調整が重要になります。性能は“速くする”だけでなく“更新頻度と整合性をどう設計するか”が本質です。

開発体験(書きやすさ・見通し)

開発体験は、コード量だけでなく「責務が分離できるか」「データ取得の流れが追えるか」が効いてきます。特にチーム開発では、データ取得の実装が散らばるほど改修コストが上がります。

  • Axios:API呼び出し関数を作って使う、という単純さが魅力です。ただし、ローディング・エラー・再試行・キャッシュ・重複リクエスト抑止などを各コンポーネントで都度考えることになり、規模拡大に伴って“呼び出しは簡単だが運用が難しい”状態になりやすいです。

  • SWR:フック中心のAPIで直感的に書け、コンポーネント内の記述はシンプルになりやすいです。反面、アプリ全体のデータ同期ルールを統一しようとすると、キー設計や再検証条件の取り扱いがケースバイケースになり、統制が難しくなる場面もあります。

  • React Query:サーバー状態を「クエリ」として扱うため、取得・更新・再取得の筋道が立ちやすいのが強みです。クエリを中心に設計できるので、複数画面で同じデータに依存しても挙動が読みやすく、変更にも強くなります。特にReact Queryは“画面ごとの状態”ではなく“データのライフサイクル”に寄せられるため、長期運用の見通しが良くなりやすいです。

実装時の体感としては、最初はSWRが軽く書け、運用が複雑になるほどReact Queryの設計メリットが効いてくる、という傾向があります。

学習コストとチーム導入のしやすさ

学習コストは「概念の多さ」だけでなく、「チームの共通言語を作れるか」に直結します。導入後に属人化しないか、レビューで判断できるか、運用ルールを定義しやすいかがポイントです。

  • Axios:HTTPクライアントとしての学習コストは低めです。一方で、キャッシュや再取得ポリシーを各自が実装すると設計思想が統一されにくく、結果的に“学習コストが低いはずが、運用コストが高い”状態になることがあります。

  • SWR:フックの使い方は理解しやすく、少人数・短期間の開発で導入しやすいです。チーム全体でのルール化(キー設計、無効化相当の運用、再取得条件の統一など)まで踏み込む場合、追加の合意形成が必要になります。

  • React Query:React Queryは概念(クエリ、キャッシュ、再取得戦略など)があり、初期学習は相対的に必要です。ただ、その分「こういう時はこの操作」という型が作りやすく、チーム開発での共通言語になりやすいのが利点です。レビュー観点も揃えやすく、運用ルールを決めることで導入効果が安定します。

“導入が簡単”か“導入後に崩れない”かは別問題です。React Queryは後者に寄せた選択肢になりやすいと言えます。

互換性(環境・既存コードとの統合)

互換性の観点では、「既存のAPI層(Axiosなど)を活かせるか」「React以外の層に影響しないか」「段階導入できるか」が重要です。全面移行が必要な設計だと、導入が止まりやすくなります。

  • Axios:ブラウザ・Node.jsなど幅広い環境で使われ、既存コード資産も多いのが強みです。API層としてAxiosを使い続けつつ、上位レイヤーでSWR/React Queryを採用する構成も一般的です(=競合ではなく補完関係)。

  • SWR:fetcherを差し替えられるため、既存のAxiosベースのAPI関数をそのまま利用しやすいです。Reactのコンポーネント単位で導入でき、段階移行も現実的です。

  • React Query:React Queryも取得関数(queryFn)に既存のAxios関数等を渡せるため統合しやすく、段階導入が可能です。既存の“データ取得ロジックが各所に散っている”場合でも、画面単位でReact Queryに寄せていく移行が取りやすいのが特徴です。React QueryはReact(および対応環境)での利用が前提になるため、非React部分の状態管理を直接置き換えるというより、UI層のデータ同期を整える用途に向きます。

まとめると、Axiosは基盤の通信レイヤーとして堅実、SWRは軽量に最新化したいUIに向き、React Query(TanStack Query)は中〜大規模でデータ同期のルールを持続的に運用したい場面で強い選択肢です。既存コードとの統合・段階導入を前提に比較すると、React Queryは“移行しながら価値を出しやすい”点でも検討に値します。

導入事例から学ぶ適用パターン(一般化)

react+query+cache

React Query(TanStack Query)は「APIから取ってきたデータを、画面の状態としてどう扱うか」を整理するための道具です。ただし、すべての画面に機械的に当てはめると、かえって複雑になることもあります。ここでは、導入事例で頻出するパターンを一般化し、React Queryが特に効くプロダクト/画面の特徴と、現場でよく起きるつまずきを整理します。

適しているプロダクト/画面の特徴

React Queryが真価を発揮するのは、「サーバーの状態(Server State)」がUIの中心にあり、かつ取得・再取得・更新の条件が複雑になりやすいプロダクトです。導入がうまくいく現場では、次の特徴が揃っていることが多いです。

  • 一覧・詳細・検索など、同じデータを複数画面で使い回す

    例:商品一覧→商品詳細→関連商品、ユーザー一覧→ユーザー詳細→権限設定など。画面を跨いで同一データを参照するほど、React Queryのキャッシュが効きやすく、体感速度や実装の見通しが上がります。

  • 「再取得が必要なタイミング」が多い

    例:タブ切り替え、フィルタ変更、ページ遷移後の再表示、一定時間ごとの更新、編集完了後の表示更新など。これらが手動実装だと分散しやすい一方、React Queryでは取得を“状態”として扱えるため、意図した再取得設計にまとめやすくなります。

  • フォーム編集・ステータス変更など「更新」が頻繁に発生する

    例:管理画面(CRUD)、ワークフロー(承認/差戻し)、SNS/チャットの送信、在庫や予約の更新など。更新後に関連する画面の表示を揃える必要があるケースほど、React Queryのキャッシュ運用が効果的です。

  • 通信エラー・権限・ネットワーク不安定など“現実の揺らぎ”を前提にしたい

    例:モバイル回線、業務端末、VPN越しの管理画面など。失敗や再試行、部分的な表示継続など、現実的な体験設計が必要な環境で、React Queryは実装の複雑化を抑えやすい傾向があります。

  • チーム開発で「取得処理の流儀」を統一したい

    画面ごとにfetchが散らばると、ローディングや再取得の挙動がバラつき、品質が落ちやすくなります。React Queryに寄せることで「この種類の画面はこのやり方」という共通認識を作りやすくなります。

逆に、導入の優先度が下がりやすいのは「完全にローカルで完結する状態(UIの開閉、入力中の一時値など)が中心」の画面です。React Queryはあくまで“サーバーの状態”に強い、という前提を押さえると適用判断がブレにくくなります。

よくあるつまずきと回避策

React Queryの導入で詰まりやすいのは、API呼び出し自体ではなく「キャッシュをどう信頼し、いつ更新するか」という運用設計です。ここでは現場で頻出する落とし穴を、回避の考え方とセットでまとめます。

  • つまずき1:画面ごとにqueryKeyがバラバラでキャッシュが効かない/衝突する

    導入初期に多いのが、同じ意味のデータなのに画面ごとに違うqueryKeyを付けてしまい、再利用できず二重取得になるケースです。逆に、雑に同じキーを使って別条件の結果が混ざる“衝突”も起こり得ます。

    回避策:「リソース名+条件」の粒度で、同じ条件なら同じキー、条件が違えば必ずキーが違うという原則で揃えます。レビュー観点として、キーの設計が“画面名”起点になっていないかを確認するとブレが減ります。

  • つまずき2:更新後に表示が古い(どのデータを更新すべきか分からない)

    ミューテーション後に「一覧に戻ったら古い」「詳細だけ更新されない」「関連ウィジェットがズレる」など、キャッシュ整合性の問題が顕在化します。

    回避策:更新が起きたときに影響を受ける“読み取りクエリ”を洗い出し、更新対象をパターン化します。特に、一覧・詳細・サマリー(件数や集計)など複数のビューが同じ実体を参照している場合は、影響範囲を先に言語化してから実装すると手戻りが減ります。

  • つまずき3:「とりあえず毎回再取得」で体感が悪化する

    安全に倒して何でも再取得すると、画面遷移や操作のたびにローディングが目立ち、ネットワーク負荷も上がります。結果として「React Queryにしたのに遅い」という評価につながります。

    回避策:“常に最新である必要がある画面”と“多少の遅延が許容される画面”を分け、後者ではキャッシュを前提にUIを組み立てます。特にダッシュボードや一覧は、ユーザーが求めるのが「即時表示」なのか「完全な最新」なのかを整理するのが有効です。

  • つまずき4:ローディング表現が過剰(チラつき)でUXが崩れる

    データがキャッシュから即座に出るケースでも毎回スピナーを出してしまい、画面が“点滅する”ような体験になることがあります。導入前は気にならなかったのに、移行後に顕在化することもあります。

    回避策:「初回表示のローディング」と「再取得中の表示」を分けて設計します。ユーザーが操作中に裏で更新しても、画面全体をブロックしない方が自然なケースが多いです。

  • つまずき5:エラーの扱いが画面ごとに違い、復帰導線がバラつく

    同じ種類の失敗でも、ある画面は無言、別の画面はモーダル、さらに別の画面はリトライ不能など、体験と実装が散らばりがちです。

    回避策:「ユーザーに見せるべきエラー」と「ログに残せばよいエラー」を分け、リトライ・再読み込み・問い合わせ導線などの基本形を決めてから展開します。React Queryの導入は、エラー設計を統一する良いタイミングになりやすいです。

  • つまずき6:SSR/ルーティング/モバイルなど環境要因で挙動が変わるのに、前提を揃えない

    同じReact Queryでも、画面の生存期間(マウント/アンマウント)や遷移設計が違うと、期待するキャッシュの残り方が変わります。結果として「この画面だけ挙動が違う」問題が起きがちです。

    回避策:“コンポーネントがどのくらいの頻度で生まれ変わるか”を前提に、画面タイプ(一覧/詳細/入力/ダッシュボード)ごとに実装指針を揃えます。環境差を無視して個別最適をすると、後から統一が難しくなります。

導入を成功させるコツは、React Queryを「便利な取得ライブラリ」として部分導入するだけでなく、画面タイプごとの“データの信頼の置き方”を揃えることです。適用範囲とパターンを一般化しておくと、チーム内で判断がぶれず、移行も運用も加速します。

まとめ(導入判断と次のアクション)

react+query+cache

React Query(TanStack Query)は「データ取得を便利にするライブラリ」というより、チーム開発で起こりがちな通信・キャッシュ・再取得の判断を“ルール化”して事故を減らすための基盤です。導入判断では、画面ごとに場当たり的にfetchしている状態から抜け出し、運用の一貫性を作れるかがポイントになります。ここでは、導入を決めた後に迷わないための「最低限の設定と運用ルール」「併用で効果が上がる周辺ツール」「段階導入の進め方」を整理します。

まず押さえるべき設定と運用ルール

React Queryを“便利なHooks集”として使い始めると、プロジェクトが大きくなるほどクエリ設定やキャッシュ運用がバラつき、結局デバッグが難しくなります。最初に決めておくべきは、全体のデフォルト設定と、チームで迷わない命名・責務分担です。

  • QueryClientのデフォルト(全体方針)を先に固定する

    画面ごとにstaleTimeretryをバラバラにしないため、まずは「原則こうする」を決めます。例えば、管理画面のように即時性が重要なら短め、読み取り中心なら長め、といった基準を作り、例外だけ各クエリで上書きします。

  • queryKeyのルールを言語化する(後から効く)

    React Queryの運用コストは、ほぼqueryKey設計で決まります。最低限、以下をチームで統一すると破綻しにくくなります。

    • キーは配列で表現し、先頭はリソース種別(例:['users', ...])にする

    • パラメータ(ページ、検索条件、ID)は必ずキーに含め、同一条件が同一キーになるようにする

    • オブジェクトを入れる場合はプロパティ順や不要値を揃え、同じ意味なら同じ形にする

  • 「どこに何を書くか」を決める(責務分離)

    HTTP呼び出し処理、React Queryのフック、UIコンポーネントが混ざると再利用とテストが難しくなります。運用ルールとして、API呼び出しは関数化し、フックはデータ境界(画面/機能単位)でまとめ、UIはデータ取得の詳細に依存しない形に寄せる、などの方針を先に置くとチームが迷いません。

  • エラーの扱いと通知方法を共通化する

    個別画面でアラートやトーストを乱立させると、ユーザー体験が統一されません。「どのエラーは画面内に出す」「どのエラーはグローバル通知」「再試行する/しない」といった運用を決め、共通の仕組みに寄せると、React Query導入の効果がはっきり出ます。

これらは細かい実装テクニックではなく、React Queryをプロダクションで運用するうえでの“土台”です。最初に最低限だけ決め、例外運用を増やしすぎないことが成功の近道になります。

併用(OpenAPI/Devtools)で得られる効果

React Queryは単体でも強力ですが、周辺ツールを併用すると「見える化」と「型安全」により、導入効果が一段上がります。ここで重要なのは、便利さ以上に運用時の事故を減らす点です。

  • Devtools併用:キャッシュと再取得の挙動が説明可能になる

    「なぜこのタイミングで再取得が走ったのか」「どのqueryKeyが残っているのか」といった、React Query特有の疑問は目視できると解決が速いです。Devtoolsがあると、クエリの状態・キャッシュの存在・更新タイミングが追えるため、オンボーディングやレビュー時のコミュニケーションコストも下がります。

  • OpenAPI併用:API変更に強くなり、実装ミスが減る

    OpenAPIを中心に型定義やクライアント生成を整えると、フロント側で「リクエストの形が違う」「レスポンスの型を勘で扱う」といったミスが減ります。結果として、React Queryのフック実装が安定し、レビューで見るべきポイントが「キャッシュ戦略やキー設計」に集中します。

  • 組み合わせの相乗効果:運用ルールを“守りやすく”できる

    Devtoolsで挙動を確認でき、OpenAPIで入出力が揃うと、チームの運用ルール(queryKey規約、再取得方針、エラー方針など)が崩れにくくなります。これは短期的な生産性よりも、中長期の保守性に効いてきます。

段階導入の進め方

既存コードがあるプロジェクトにReact Queryを一気に適用すると、移行途中でルールが崩れたり、画面ごとに実装が揺れたりしがちです。段階導入では「まず基盤を作る → 価値が出やすい箇所から置き換える → ルールを固める」の順に進めると失敗しにくくなります。

  1. Step 1:最小の基盤を先に作る(ブレない土台)

    QueryClientの用意とProvider適用、デフォルトオプションのたたき台、queryKey命名規約(最低限)をまず決めます。この時点では、全画面を移行する必要はありません。「新規実装はReact Queryを使う」を宣言できる状態を作るのが目的です。

  2. Step 2:読み取り中心の画面から適用する(リスクが低い)

    一覧・詳細など取得系(クエリ)から置き換えると、導入効果(ローディング/エラー/再取得の統一)が見えやすく、既存ロジックへの影響も抑えられます。ここで「同じ用途の画面は同じ書き方」に寄せ、実装例をチームの参照点にします。

  3. Step 3:更新系は“整合性ルール”を決めてから広げる

    更新処理(ミューテーション)は、更新後の反映方法が統一されないとUIが不安定になります。段階導入では、更新後の反映方針(再取得で揃えるのか、キャッシュ更新を使うのか)をチームで先に合意してから対象範囲を広げるのが安全です。

  4. Step 4:導入範囲が広がったら、運用ルールを“文章化”して固定する

    段階導入の終盤で効くのがドキュメント化です。queryKey規約、例外設定の判断基準、エラーの扱い、レビュー観点などを短くても良いので明文化すると、属人化せずにReact Query運用が回り始めます。

React Queryの導入は、ライブラリ追加よりも「運用の標準化プロジェクト」に近いです。小さく始めて成功パターンを作り、ルールを固めながら適用範囲を広げることが、最短で効果を出す次のアクションになります。