← プログラミングページの先頭へ

TanStack Query: React サーバーの状態管理

TanStack Query (旧名 React Query) は、非同期サーバーの状態を管理するためのライブラリです。

主なソリューション:

  • サーバーのデータ取得。
  • リクエストの状態管理。
  • データのキャッシュと再利用。
  • 自動的に再リクエストします。
  • 重複排除を要求します。
  • サーバーデータの変更。
  • 更新と無効化をキャッシュします。
  • ページネーションと無限スクロール。

Django + DRF + Orval + Axios + React プロジェクトの場合:

Django / DRF
      ↑
    Axios
      ↑
Orval Generated API Function
      ↑
TanStack Query
      ↑
Orval Generated Hooks
      ↑
React Components

区別する必要があります:

ツール中核的な責任
AxiosHTTP リクエストを送信する
OrvalOpenAPI から API 関数と Hooks を生成
TanStack Queryサーバーの状態とキャッシュを管理する
ReactUI レンダリングとコンポーネントの状態

TanStack Query は実際に HTTP リクエストを送信する責任はありませんが、queryFn、mutationFn を通じて Axios およびその他のリクエスト関数を呼び出します。

1. QueryClient および QueryClientProvider

QueryClient は TanStack Query のコア オブジェクトであり、Query Cache、Mutation Cache、およびグローバル デフォルト設定を管理するために使用されます。

通常、アプリケーションの入り口で作成されます。

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,
      gcTime: 5 * 60_000,
      retry: 2,
      refetchOnWindowFocus: true,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MainApp />
    </QueryClientProvider>
  );
}

コンポーネント内で使用します。

const queryClient = useQueryClient();

現在Providerが管理しているQueryClientを入手できます。

QueryClientの共通メソッド

方法機能
invalidateQueries()タグキャッシュの無効化
setQueryData()指定されたキャッシュを直接変更します
getQueryData()同期読み取りキャッシュ
removeQueries()一致するキャッシュを削除する
resetQueries()Queryをリセット
refetchQueries()一致する Query を取得
cancelQueries()マッチングリクエストをキャンセルする
fetchQuery()データを取得し、必要に応じてリクエストする
prefetchQuery()事前にデータを取得してキャッシュする
clear()Query および Mutation キャッシュをクリアします

一般に、アプリケーションのライフサイクル中は安定した QueryClient インスタンスを維持し、コンポーネントがレンダリングされるたびにインスタンスを再作成しないでください。

2. useQuery: サーバーデータの読み取り

useQuery() は主にサーバー データの取得とキャッシュに使用されます。

const result = useQuery({
  queryKey: ["user", userId],
  queryFn: () => getUser(userId),
  enabled: userId != null,
  staleTime: 60_000,
  gcTime: 300_000,
});

コアパラメータ:

パラメータ機能
queryKey一意の識別子のクエリとキャッシュ
queryFnデータを取得する非同期関数
enabledクエリを自動的に実行するかどうか
staleTimeデータが新鮮な状態を保つ期間
gcTime未使用のキャッシュ保持時間
retryリクエストが失敗した後の再試行戦略
selectHook によって返されたデータを変換します
placeholderData一時的に表示されるプレースホルダーデータ
initialData初期キャッシュデータの設定

Queryステータス

const {
  data,
  error,
  isPending,
  isLoading,
  isFetching,
  isError,
  isSuccess,
  refetch,
} = useQuery({
  queryKey: ["user", userId],
  queryFn: () => getUser(userId),
});
ステータス意味
isPendingデータはまだ正常に取得されていません
isLoading最初のデータ取得が進行中です
isFetching現在リクエスト中(バックグラウンド更新を含む)
isRefetching初期ロードではなく再フェッチ
isError現在のクエリステータスはエラーです
isSuccess現在のクエリステータスは成功です
isStaleデータの有効期限が切れているか、アクティブに無効化されています

特別な注意:

isLoading = isPending && isFetching;

したがって、enabled: false でデータがない場合、次のような現象が発生する可能性があります。

isPending = true
isLoading = false
isFetching = false

Query にはまだデータがありませんが、リクエストは行われていないため、これは矛盾しません。

代表的なコンポーネント

function UserProfile({ userId }: { userId: number }) {
  const { data, isPending, isError, error, isFetching } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => getUser(userId),
  });

  if (isPending) return <div>Loading...</div>;
  if (isError) return <div>{error.message}</div>;

  return (
    <div>
      <h2>{data.username}</h2>
      {isFetching && <span>Refreshing...</span>}
    </div>
  );
}

古いデータがすでに利用可能な場合、コンポーネントは、バックグラウンドで再要求された場合でも、キャッシュされたコンテンツを表示できます。

3. Query Key: キャッシュID

TanStack Query 異なるクエリを区別するには、配列形式 queryKey を使用します。

["user", 1][("user", 2)][("pois", { city: 1, page: 1 })][
  ("pois", { city: 1, page: 2 })
];

異なる Query Key は異なるキャッシュ エントリに対応します。

Query Keyレベルマッチング

たとえば:

["user"][("user", "me")][("user", "profile")][("user", "profile", 1)];

実行:

queryClient.invalidateQueries({
  queryKey: ["user"],
});

デフォルトでは接頭辞一致が使用されるため、上記のすべての Query と一致します。

完全一致のみが必要な場合:

queryClient.invalidateQueries({
  queryKey: ["user"],
  exact: true,
});

Query Key 設計原則

クエリ結果が依存するパラメータは通常、Query Key に含める必要があります。

たとえば:

useQuery({
  queryKey: ["pois", cityId, page],
  queryFn: () => getPois(cityId, page),
});

Query Key に cityId が含まれていない場合、異なる都市のデータが誤って同じキャッシュを共有する可能性があります。

Hook を生成する Orval の場合、通常は自動生成された Query Key ファクトリが優先されます。

4. staleTimeとgcTime

これらは、TanStack Query の 2 つの最も重要なキャッシュ パラメーターです。

staleTime: フレッシュタイム

staleTime: 60_000;

取得または更新が成功した後、通常 60 秒間、データが fresh とみなされることを示します。

データには主に 2 つの状態があります。

Fresh
  ↓ staleTime 有効期限
Stale
  • Fresh: データは新しいものとみなされ、コンポーネントのマウントやウィンドウのフォーカスなどのデフォルトのトリガー条件により、通常は再要求されません。
  • Stale: データの有効期限が切れているため、適切なタイミングで自動的に再リクエストできます。

Stale は、キャッシュが削除されたことを意味するものではなく、また、期限切れの直後にリクエストを送信する必要があることを意味するものでもありません。

gcTime: ガベージ コレクション時間

gcTime: 300_000;

通常、Query にアクティブなサブスクライバーがいなくなった後、未使用のキャッシュが 5 分間残ることを示します。

Component Mounted
        ↓
     Active Query
        ↓
Component Unmounted
        ↓
    Inactive Query
        ↓
      gcTime
        ↓
   Cache Removed

まだキャッシュ内にある Query を再サブスクライブすると、再び Active 状態に入ります。

両者の違い

コントラストstaleTimegcTime
制御内容データは新しいですか?キャッシュはいつ削除されますか?
メイン開始時間データが正常に取得または更新されましたQuery が Inactive になります
有効期限切れの結果データはStaleになりますキャッシュを再利用できる
すぐにリクエストするかどうかいいえいいえ
キャッシュを削除するかどうかいいえはい

例

仮定:

staleTime: 60_000,
gcTime: 300_000,

クエリの有効期限が切れていても、コンポーネントが常にそのクエリにサブスクライブされている場合、gcTime の有効期限が切れたからといってキャッシュは削除されません。

コンポーネントがアンマウントされ、2 分後に再マウントされた場合:

  • キャッシュはまだ存在します。
  • データは Stale だった可能性があります。
  • デフォルトでは、古いキャッシュはすぐに表示され、バックグラウンドで更新されます。

アンインストールから 5 分以上経過し、他のサブスクライバーまたは保持メカニズムがない場合:

  • 元の Query はガベージ コレクションされた可能性があります。
  • ページに再度アクセスするには、通常、新しいリクエストが必要です。

デフォルト値

TanStack Query v5 共通のデフォルト設定:

staleTime: 0;
gcTime: 5 * 60 * 1000;

意味:

  • データのデフォルトは直ちに Stale になります。
  • Inactive Query はデフォルトで 5 分間保持されます。

これは、staleTime が構成されていない場合に、ページ エントリを繰り返すとリクエストが継続的にトリガーされる理由を説明しています。

Infinity および static

staleTime: Infinity;

データは時間の経過によって自動的に古くなることはありませんが、invalidateQueries() によって無効化することはできます。

staleTime: "static";

これにはより厳密な静的データ セマンティクスがあり、通常の無効化操作では無効化をトリガーできません。

アプリケーションの実行中に実際に変更されないデータに適しています。

5. ActiveとInactive Query

Active

現在、Query Observer がサブスクライブされ有効になっています。

たとえば:

function Profile() {
  const result = useUserProfileRetrieve();
  // ...
}

Hook が Query をサブスクライブして有効にすると、通常は Active 状態になります。

Inactive

現在、アクティブな Query Observer はありません。

たとえば、ユーザーが Profile ページを離れると、関連コンポーネントはアンインストールされ、他のコンポーネントはそのコンポーネントをサブスクライブしません。

ただし、Query はキャッシュ内にまだ存在する可能性があります。

Query ライフサイクル

useQuery Mount
      ↓
Active
      ↓
Fetch / Cache
      ↓
Component Unmount
      ↓
Inactive
      ↓
gcTime Expires
      ↓
Garbage Collection

注: active/inactive はサブスクリプションのステータスを示します。 fresh/stale はデータの鮮度を示します。それらは 2 つの独立した次元です。

6. 自動再リクエスト(Refetch)

TanStack Query は、デフォルトで特定の時間に Stale データを再要求します。

トリガー条件構成
コンポーネントの再マウントrefetchOnMount
ブラウザウィンドウのフォーカスが再設定されましたrefetchOnWindowFocus
ネットワークの再接続refetchOnReconnect
定期的なポーリングrefetchInterval
手動で再リクエストするrefetch()
キャッシュの無効化invalidateQueries()

たとえば:

useQuery({
  queryKey: ["user", "profile"],
  queryFn: getProfile,
  staleTime: 60_000,
  refetchOnWindowFocus: false,
  refetchOnReconnect: true,
  refetchOnMount: true,
});

refetchInterval

refetchInterval: 30_000;

約 30 秒ごとにデータの再取得を試みることを示します。

スケジュールされたポーリングは staleTime によって完全に制限されているわけではなく、独立した自動要求メカニズムであることに注意してください。

enabled

useQuery({
  queryKey: ["user", userId],
  queryFn: () => getUser(userId!),
  enabled: userId != null,
});

これは、有効な userId を取得した後にのみリクエストが自動的に送信されることを意味します。

無効な状態は自動無効化と再フェッチの動作にも影響するため、単にリクエストを遅らせるものとして解釈しないでください。

7. useMutation: サーバーステータスの変更

useMutation() は通常、データの作成、更新、または削除に使用されます。

const mutation = useMutation({
  mutationFn: updateProfile,
});

実行:

mutation.mutate({
  username: "Tom",
});

Mutationステータス

ステータス意味
isIdleまだ実行されていません
isPending実行中
isSuccess成功
isError失敗しました
dataデータが正常に返されました
errorエラーオブジェクト

mutate および mutateAsync

mutation.mutate(data);

Mutation をトリガーし、コールバックを通じて結果を処理します。

await mutation.mutateAsync(data);

Promise を返します。これを try/catch と組み合わせて結果を処理できます。

try {
  await mutation.mutateAsync({
    username: "Tom",
  });
} catch (error) {
  console.error(error);
}

Mutation は、Query のように、デフォルトでは Query Key 単位で長期サーバー データ キャッシュを自動的に維持しません。

8. Mutation Lifecycle

Mutation は 4 つの重要なライフサイクル コールバックを提供します。

コールバック通話時間
onMutate実行前 mutationFn
onSuccessリクエストは成功しました
onErrorリクエストが失敗しました
onSettledリクエストが成功または失敗した後

onSuccess

const mutation = useMutation({
  mutationFn: updateProfile,
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: ["user", "profile"],
    });
  },
});

書き込みが成功した後に関連するキャッシュをフラッシュするのに適しています。

onError

onError: (error) => {
  console.error(error);
};

失敗の処理、エラーの表示、またはオプティミスティックな更新のロールバックに使用されます。

onSettled

onSettled: () => {
  return queryClient.invalidateQueries({
    queryKey: ["user", "profile"],
  });
};

成功しても失敗しても、最終的には実行されます。

コールバックが Promise を返した場合、TanStack Query は対応する Mutation ライフサイクル処理を終了する前に、コールバックが完了するまで待機します。

注: 失敗時にデータを更新する必要がない場合は、常に onSettled でサーバーに要求する必要はありません。

9. 楽観的なアップデート (Optimistic Update)

楽観的な更新とは次のことを指します。

サーバーが成功を確認する前に、クライアント インターフェイスまたはキャッシュを更新します。

たとえば、ユーザーがニックネームを変更するとします。

User Changes Name
       ↓
Update UI Immediately
       ↓
Send PATCH Request
       │
       ├── Success → Keep / Synchronize
       │
       └── Error → Rollback

完全な例

const queryClient = useQueryClient();

const profileKey = ["user", "profile"] as const;

const mutation = useMutation({
  mutationFn: (newName: string) => updateProfile({ username: newName }),

  onMutate: async (newName) => {
    await queryClient.cancelQueries({
      queryKey: profileKey,
    });

    const oldProfile = queryClient.getQueryData<{
      username: string;
    }>(profileKey);

    queryClient.setQueryData(profileKey, (old: typeof oldProfile) =>
      old ? { ...old, username: newName } : old,
    );

    return { oldProfile };
  },

  onError: (_error, _variables, onMutateResult) => {
    if (onMutateResult?.oldProfile !== undefined) {
      queryClient.setQueryData(profileKey, onMutateResult.oldProfile);
    }
  },

  onSettled: () => {
    return queryClient.invalidateQueries({
      queryKey: profileKey,
    });
  },
});

3 つの重要なステップ:

  1. cancelQueries(): 古いリクエストが返された後にオプティミスティック更新を上書きしないようにします。
  2. setQueryData(): クライアント キャッシュを直ちに変更します。
  3. onError(): リクエストが失敗した場合に以前のデータを復元します。

onMutate() の戻り値を後続のコールバックに渡して、古いキャッシュ データを保存できます。

注: 複雑な同時実行性 Mutation は更新順序の競合を引き起こす可能性があるため、楽観的な更新がすべてのビジネスに適しているとは限りません。

さらに、TanStack Query v5 では、useMutation() で onMutate を構成する必要があります。 mutate() のコールレベルのコールバックには、主に onSuccess、onError、onSettled が含まれます。

10. invalidateQueries: キャッシュの無効化

queryClient.invalidateQueries({
  queryKey: ["user", "profile"],
});

機能:

  1. 指定された Query と一致します。
  2. Stale というラベルを付けます。
  3. デフォルトでは、条件を満たす Active Query に対してバックグラウンド再取得がトリガーされます。
Server Data Updated
        ↓
invalidateQueries()
        ↓
Cache Marked Stale
        │
        ├── Active → Refetch
        │
        └── Inactive → Wait for Future Usage

障害の範囲

queryClient.invalidateQueries({
  queryKey: ["user"],
});

["user"] というプレフィックスが付いた複数の Query と一致できます。

完全一致:

queryClient.invalidateQueries({
  queryKey: ["user", "profile"],
  exact: true,
});

refetchType

queryClient.invalidateQueries({
  queryKey: ["user"],
  refetchType: "all",
});

一般的なオプション:

  • active: デフォルト。アクティブな Query に一致する再リクエスト。
  • inactive: 非アクティブな Query に一致するリクエスト。
  • all:一致するアクティブおよび非アクティブな Query をすべて再取得します。
  • none: 失敗をマークするだけで、再リクエストはトリガーされません。

特別な静的構成を使用して無効化された Query および Query には追加の制限があります。

11. setQueryData: キャッシュを直接変更する

setQueryData() は HTTP リクエストを送信せず、Query Cache を直接変更します。

queryClient.setQueryData(["user", "profile"], {
  username: "Bob",
  avatar: "avatar.jpg",
});

Query が存在しない場合、有効なデータが提供されたときにキャッシュ エントリが作成される可能性があります。

成功後に Mutation がキャッシュに書き込まれます

const mutation = useMutation({
  mutationFn: updateProfile,

  onSuccess: (updatedProfile) => {
    queryClient.setQueryData(["user", "profile"], updatedProfile);
  },
});

これにより、サーバーが完全で信頼できる最新のデータを返す場合、追加の GET リクエストを減らすことができます。

機能アップデート

queryClient.setQueryData(
  ["user", "profile"],
  (old: UserProfile | undefined) => {
    if (!old) return old;

    return {
      ...old,
      username: "Bob",
    };
  },
);

キャッシュ内の古いオブジェクトを直接変更しないでください。

old.username = "Bob";
return old;

新しいオブジェクトを作成するには、不変の更新を使用する必要があります。

12. getQueryData、removeQueries、resetQueries

getQueryData

既存のキャッシュを同期的に読み取ります。

const profile = queryClient.getQueryData<UserProfile>(["user", "profile"]);

HTTP リクエストは積極的に送信されません。

removeQueries

queryClient.removeQueries({
  queryKey: ["user", "profile"],
});

一致するキャッシュ レコードを削除します。

不要になったデータをクリーンアップするのに適していますが、コンポーネントのサブスクリプションがまだある Query の場合、削除後のコンポーネントのステータスと後続のリクエストを考慮する必要があります。

resetQueries

queryClient.resetQueries({
  queryKey: ["user", "profile"],
});

マッチ Query を元の状態に戻します。

initialData で Query を設定すると、この初期データに戻すことが可能です。

通常、アクティブな Query も再リクエストされます。

clear

queryClient.clear();

Query Cache と Mutation Cache をクリーンアップします。

これは、クライアント サーバーのステータスを全体としてクリアする必要があるが、まだ必要なデータを無差別にクリアすることは避けるべきシナリオに適しています。

コントラスト

方法キャッシュの削除リクエスト
invalidateQueriesいいえデフォルトでは、条件を満たすActive Queryが更新されます。
setQueryDataいいえいいえ
removeQueriesはい更新を自動的に開始しない
resetQueriesリセット状態Active Query は通常更新されます
clearキャッシュをクリアする更新を自動的に開始しない

13. 自動キャッシュとリクエストの重複排除

2 つのコンポーネントが同時に同じデータを要求した場合:

function Header() {
  useUserProfileRetrieve();
}

function ProfilePage() {
  useUserProfileRetrieve();
}

同じ QueryClient と Query Key を使用する限り:

Component A ──┐
              ├── Shared Query Cache
Component B ──┘
                      ↓
                 Single Query
                      ↓
                  HTTP Request

TanStack Query は通常、同じリクエストの 2 つのコピーを送信するのではなく、同じ実行中のリクエストを共有します。

ただし、注意してください:

  • 異なる Query Key では、重複は自動的に削除されません。
  • リクエスト完了後に再度リクエストするかどうかは、鮮度と設定によって異なります。
  • 通常のメモリ キャッシュは、2 つの異なる QueryClient 間で共有されません。

14. Retry: 失敗後の再試行

TanStack Query は、クライアント環境で失敗した Query をデフォルトで再試行します。共通のデフォルト回数は 3 回です。

retry: 3;

オフにすることができます:

retry: false;

エラーの種類に基づいて判断することもできます。

retry: (failureCount, error) => {
  const status = axios.isAxiosError(error) ? error.response?.status : undefined;

  if (status === 400 || status === 401 || status === 403) {
    return false;
  }

  return failureCount < 2;
};

一般原則:

  • 一時的なネットワーク障害: もう一度試してください。
  • サーバーは一時的に利用できません。再試行することを検討してください。
  • パラメータ エラー: 通常、再試行する必要はありません。
  • 認証または権限エラー: 通常、再試行は必要ありません。

useMutation はデフォルトでは自動的に再試行しないため、Query の再試行ポリシーを書き込み操作に無差別に使用しないでください。

非冪等書き込みリクエストを自動的に再試行する場合は、特に注意が必要です。

15. selectのQuery

select は、Hook によって返されたデータを変換するために使用されます。

たとえば、バックエンドは次を返します。

{
  id: 1,
  username: "Alice",
  email: "alice@example.com"
}

ユーザー名だけを抽出することもできます。

const { data: username } = useQuery({
  queryKey: ["user", "profile"],
  queryFn: getProfile,
  select: (data) => data.username,
});

注:

  • select は、この Query Observer によって返されるデータに影響します。
  • Query Cache の元のデータは、選択した値に直接置き換えられません。

複数のコンポーネントが同じサーバー データから異なるフィールドを読み取る状況に適しています。

16. initialDataとplaceholderData

initialData

useQuery({
  queryKey: ["user", 1],
  queryFn: getUser,
  initialData: {
    id: 1,
    username: "Alice",
  },
});

initialDataはQuery Cacheに初期値を入れます。

信頼できる初期データがすでに存在する状況に適しています。

placeholderData

useQuery({
  queryKey: ["user", 1],
  queryFn: getUser,
  placeholderData: {
    id: 1,
    username: "Loading...",
  },
});

placeholderData は一時的な表示にのみ使用され、正式なクエリ結果として Query Cache に書き込まれることはありません。

違い

特長initialDataplaceholderData
Query Cacheと書くはいいいえ
実際の初期データとしてはいいいえ
一時的な UI プレースホルダに適しています一般的には優先事項ではありませんはい
鮮度判定に影響はありますか?はい実際のデータとしてキャッシュされない

17. ページングと keepPreviousData

API のページングの例:

GET /api/v1/pois?page=1
GET /api/v1/pois?page=2

ページングの切り替え時に古いデータがすぐに破棄されると、インターフェイスが一時的に空白になることがあります。

TanStack Query v5 は、placeholderData および keepPreviousData と一緒に使用できます。

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

const result = useQuery({
  queryKey: ["pois", page],
  queryFn: () => getPois(page),
  placeholderData: keepPreviousData,
});

機能:

Page 1 Data
     ↓
User Switches to Page 2
     ↓
Keep Showing Page 1 Temporarily
     ↓
Fetch Page 2
     ↓
Display Page 2

isPlaceholderData を使用すると、現在表示されているデータがプレースホルダー データであるかどうかを判断できます。

これは通常のリストのページネーションに最適です。

18. useInfiniteQuery: 無限スクロール

無限スクロール、さらなる読み込みなどのシナリオでは、useInfiniteQuery を使用できます。

const query = useInfiniteQuery({
  queryKey: ["pois", "infinite"],

  queryFn: ({ pageParam }) => getPois({ cursor: pageParam }),

  initialPageParam: null as string | null,

  getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});

中心となる概念:

プロパティ機能
pages読み込まれたページのリスト
pageParams各ページに使用されるページネーションパラメータ
fetchNextPage()次のページを取得
hasNextPage次のページはありますか?
isFetchingNextPage次のページに進みますか?
getNextPageParam次のページのパラメータを計算する

getNextPageParam から null または undefined が返されるのは、通常、次のページがないことを意味します。

Map POI の大きなデータ リストや検索結果の場合、無限スクロールが考慮される場合がありますが、すべてのインターフェイスで必ずしも必要というわけではありません。

19. AbortSignal: キャンセルのリクエスト

TanStack Query は AbortSignal を Query Function に提供します。

useQuery({
  queryKey: ["user", userId],
  queryFn: ({ signal }) =>
    axios.get(`/api/v1/users/${userId}`, {
      signal,
    }),
});

リクエストがキャンセルされた場合、Axios は Signal を消費してクライアントの HTTP リクエストを終了できます。

一般的なシナリオ:

  • ユーザーはクエリ条件を素早く切り替えることができます。
  • ページが離れます。
  • 手動でキャンセルしてください。
  • 古いリクエストはもう必要ありません。

注:

  • signal の存在は、すべての非アクティブなリクエストが自動的に中止されることを意味するものではありません。
  • リクエスト関数は実際に Signal を使用する必要があります。
  • クライアントリクエストを中止しても、バックエンドが実行を停止したことは保証されません。

Orval によって生成された API 関数は、通常、TanStack Query と組み合わせて Signal を渡すことができます。

20. Prefetch: 事前にデータを取得する

prefetchQuery() は事前にキャッシュを埋めることができます。

たとえば、ユーザーが詳細ページを開こうとしているとします。

await queryClient.prefetchQuery({
  queryKey: ["poi", poiId],
  queryFn: () => getPoi(poiId),
  staleTime: 60_000,
});

次に、コンポーネントが実行されます。

useQuery({
  queryKey: ["poi", poiId],
  queryFn: () => getPoi(poiId),
  staleTime: 60_000,
});

キャッシュがまだ新しい場合は、通常、事前に取得したデータを再度リクエストすることなく使用できます。

一般的なトリガー方法:

  • マウスHover。
  • リスト項目が表示領域に入ります。
  • ルートナビゲーション前。
  • ページのプリロード。

注: プリロードはネットワーク リソースも消費するため、すべてのページのデータを無差別にプリロードしないでください。

21. Structural Sharing

TanStack Query のデフォルトでは、古いデータと新しいデータの変更されていない参照を可能な限り再利用します。

たとえば、サーバーから返された一部のフィールドは変更されていません。

{
  user: {
    id: 1,
    username: "Alice"
  },
  total: 10
}

新しい応答が古いデータと同じである場合、TanStack Query は関連するオブジェクト参照を再利用して、不必要な参照の変更を減らすことができます。

これをStructural Sharingといいます。

主に JSON 互換のデータ構造で動作します。

JSON 以外の特殊なクラスまたは型のインスタンスを含むデータについては、制限を考慮する必要があります。

通常は、デフォルト設定をそのまま使用します。

22. Query CacheとMutation Cache

TanStack Query 内には 2 つの主要なキャッシュ管理構造があります。

Query Cache

読み取り操作のデータとステータスを管理します。

QueryCache
   ├── ["user", "profile"]
   ├── ["pois", { city: 1 }]
   └── ["itinerary", 123]

担当者:

  • データの再利用。
  • 鮮度管理。
  • クエリのライフサイクル。
  • バックグラウンドでの再リクエスト。
  • キャッシュのクリーニング。

Mutation Cache

Mutationの実行状況や履歴を管理します。

MutationCache
   ├── Update Profile
   ├── Delete POI
   └── Create Itinerary

各 Mutation の戻り結果を関連する Query Cache に自動的に同期することはありません。

Mutation が成功した後、多くの場合、積極的に呼び出す必要があるのはこのためです。

queryClient.invalidateQueries(...)

または:

queryClient.setQueryData(...)

23. グローバルコンフィグレーションとOrvalの関係

Orval を使用するプロジェクトでは、通常、生成された Query Hook を自分で再実装する必要はありません。

たとえば、Orval が生成されます。

useUserProfileRetrieve();

直接使用できます。

const { data, isPending } = useUserProfileRetrieve({
  query: {
    staleTime: 60_000,
    enabled: true,
  },
});

特定のコール署名は、Orval で生成されたコードの影響を受けます。

統合された構成が必要な場合:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,
      gcTime: 300_000,
      retry: 2,
      refetchOnWindowFocus: false,
    },
    mutations: {
      retry: 0,
    },
  },
});

プロジェクトでは一般的に次のことが推奨されます。

  • グローバル設定は、ほとんどのインターフェイスのデフォルトの動作に適しています。
  • 特殊なインターフェイスは Hook Options でカバーされます。
  • フェイルオーバー操作には Orval Query Key ファクトリを使用します。
  • Generated Code を直接変更しないでください。

24. よくある質問

Q1: TanStack Query を使用した後も、React State は必要ですか?

必要。

TanStack Query は主にサーバーの状態を管理します。

React useState は現在も次の用途に使用されています。

  • Modal が開いているかどうか。
  • フォーム入力。
  • 現在Tab。
  • ローカル UI インタラクション。
  • ユーザーが編集した、まだ送信されていないデータ。

React State をすべて TanStack Query に入れることはお勧めできません。

Q2: staleTime の有効期限が切れたらすぐにリクエストが送信されますか?

通常はそうではありません。

データを Stale にするだけです。その後、コンポーネントがマウントされたとき、ウィンドウがフォーカスされたとき、ネットワークが再接続されたとき、またはその他の設定されたタイミングが発生したときに、再取得されます。

Q3: gcTime は有効期限が切れたら再リクエストされますか?

しません。

これはガベージ コレクションを担当し、再フェッチは行いません。

キャッシュが削除されている場合、その後の Query への再サブスクリプションには通常、新しいリクエストが必要になります。

Q4: 2 つのコンポーネントが同じ Hook を呼び出す場合、2 つのリクエストが送信されますか?

同じ QueryClient と同じ Query Key に対する同時リクエストの場合、通常、リクエストは再利用されます。

ただし、リクエストが終了し、キャッシュが Stale にある場合は、後続のマウントによって再フェッチがトリガーされる可能性があります。

Q5: Mutation が成功すると、GET キャッシュは自動的に更新されますか?

通常はそうではありません。

プロジェクトが自動無効化ポリシーで追加構成されていない限り、キャッシュの一貫性は invalidateQueries() または setQueryData() を通じて手動で維持する必要があります。

Q6: invalidateQueries をいつ使用するか、setQueryData をいつ使用するか?

  • 最新データはサーバーinvalidateQueries()から再確認する必要があります。
  • Mutation は完全な最新データを返しました: setQueryData() が利用可能です。
  • 複数の関連クエリを同時に更新する必要があるため、包括的なキャッシュ戦略を設計する必要があります。

概要

TanStack Query の中心となるコンセプトは 4 つの部分に分けることができます。

分類コアコンセプト
データ読み取りuseQuery、Query Key、QueryFn、Query ステータス
キャッシュメカニズムstaleTime、gcTime、Active/Inactive、Structural Sharing
データの変更useMutation、Lifecycle、Optimistic Update
キャッシュ制御invalidateQueries、setQueryData、resetQueries、Prefetch

コアデータプロセス:

React Component
       ↓
useQuery / useMutation
       ↓
QueryClient
       │
       ├── QueryCache
       └── MutationCache
       ↓
queryFn / mutationFn
       ↓
Axios
       ↓
Django API

最もマスターする必要があるもの:

  1. Query Key キャッシュを一意に識別する方法。
  2. staleTimeとgcTimeの違い。
  3. Active / Inactive と Fresh / Stale の関係。
  4. Query データを自動再取得する方法。
  5. Mutation 成功後にキャッシュを更新または無効にする方法。
  6. onMutate、onError、および楽観的なアップデート。
  7. Orvalで生成したHooksからQueryClientを再利用する方法。

参考資料