zukucode
主にWEB関連の情報を技術メモとして発信しています。

React TanStack Queryで更新後のキャッシュを再取得する

TanStack Queryでデータを更新しても、取得済みの一覧や詳細は自動では書き換わりません。

更新後に古いデータが表示されないよう、関連するQueryを無効化して再取得します。

今回は商品一覧と商品詳細を例に、query keyの作り方と更新後の処理を紹介します。

更新レスポンスだけでキャッシュを書き換える方が通信は少なくなりますが、一覧の並び順、絞り込み、集計などをクライアントで正確に再現できない場合があります。この記事ではサーバーを最新状態の正として再取得する方法を基本にし、更新結果だけで確実に置き換えられる詳細にはsetQueryDataを使い分けます。

query keyを1か所で定義する

一覧と詳細のキーを階層化します。

productKeys.ts
export const productKeys = {
  all: ['products'] as const,
  lists: () => [...productKeys.all, 'list'] as const,
  list: (category: string) => [...productKeys.lists(), { category }] as const,
  details: () => [...productKeys.all, 'detail'] as const,
  detail: (productId: string) => [...productKeys.details(), productId] as const,
};

先頭部分を共有すると、対象範囲を選んで無効化できます。

指定するキー対象
productKeys.all商品に関するすべてのQuery
productKeys.lists()条件が異なる商品一覧すべて
productKeys.detail('p1')商品p1の詳細

一覧と詳細を取得する

productApiは、一覧取得、詳細取得、登録、更新、削除を行うAPIクライアントを表します。以下の例では、各メソッドがHTTPエラー時に例外を投げ、成功時にJSONを返すものとします。

productQueries.ts
import { useQuery } from '@tanstack/react-query';
import { productApi } from './productApi';
import { productKeys } from './productKeys';

export const useProducts = (category: string) => useQuery({
  queryKey: productKeys.list(category),
  queryFn: () => productApi.findAll(category),
});

export const useProduct = (productId: string | undefined) => useQuery({
  queryKey: productKeys.detail(productId ?? ''),
  enabled: productId !== undefined,
  queryFn: () => {
    if (!productId) throw new Error('productId is required.');
    return productApi.find(productId);
  },
});

絞り込み条件もquery keyへ含めます。含めない場合、異なる条件の一覧が同じキャッシュを共有してしまいます。

登録後に一覧を再取得する

商品を登録した場合は一覧が変わるため、すべての商品一覧を無効化します。

useCreateProduct.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { productApi } from './productApi';
import { productKeys } from './productKeys';

export const useCreateProduct = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: productApi.create,
    onSuccess: async () => {
      await queryClient.invalidateQueries({
        queryKey: productKeys.lists(),
      });
    },
  });
};

invalidateQueriesは対象を古いデータとして扱い、表示中のQueryを再取得します。

更新後に一覧と詳細を再取得する

商品名や価格を更新した場合、一覧と詳細の両方へ影響します。

useUpdateProduct.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { productApi } from './productApi';
import { productKeys } from './productKeys';

export const useUpdateProduct = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: productApi.update,
    onSuccess: async (_result, variables) => {
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: productKeys.lists() }),
        queryClient.invalidateQueries({
          queryKey: productKeys.detail(variables.productId),
        }),
      ]);
    },
  });
};

onSuccessの第2引数から、mutationへ渡した値を取得できます。

互いに依存しない再取得はPromise.allで待ちます。画面遷移前に最新化を完了させたい場合は、onSuccessをasyncにして待機します。

削除した詳細キャッシュを取り除く

削除後は対象の詳細が存在しないため、再取得ではなくキャッシュを削除します。

useDeleteProduct.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { productApi } from './productApi';
import { productKeys } from './productKeys';

export const useDeleteProduct = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: productApi.delete,
    onSuccess: async (_result, productId) => {
      queryClient.removeQueries({
        queryKey: productKeys.detail(productId),
      });
      await queryClient.invalidateQueries({
        queryKey: productKeys.lists(),
      });
    },
  });
};

詳細画面を表示したまま削除する場合は、削除後に一覧へ移動してからキャッシュを削除するなど、画面遷移との順序も決めます。

setQueryDataとの使い分け

更新APIが更新後の商品を返す場合は、詳細キャッシュを直接更新できます。

詳細キャッシュを直接更新する
onSuccess: async (updatedProduct) => {
  queryClient.setQueryData(
    productKeys.detail(updatedProduct.productId),
    updatedProduct,
  );
  await queryClient.invalidateQueries({ queryKey: productKeys.lists() });
},

詳細は即時に更新し、並び順や件数への影響がある一覧はサーバーから再取得しています。

毎回productKeys.allを無効化すると、変更と関係のない詳細まで再取得されます。

更新によって変化するデータを確認し、一覧、特定の詳細、関連する集計など必要な範囲を指定します。

確認するポイント

操作キャッシュ処理
登録一覧を無効化
更新一覧と対象の詳細を無効化
削除対象の詳細を削除し、一覧を無効化
更新結果をAPIが返す詳細へsetQueryDataも利用可能

query keyを階層化しておくと、更新の影響範囲に合わせてキャッシュを扱えます。


関連記事

  • React TanStack Queryでバックグラウンド処理をポーリングする

    CSVファイルのインポートなど、完了まで時間がかかる処理をHTTPリクエストの中ですべて実行すると、タイムアウトする可能性があります。このような処理では、開始APIから処理IDを返し、ブラウザから状態...


  • React TanStack Queryで初期データを複数のキャッシュへ分配する

    管理画面の初期表示で、店舗、商品カテゴリ、配送方法など複数のデータが必要になる場合があります。それぞれのAPIを同時に呼び出す代わりに、初期表示用APIでまとめて取得するとリクエスト数を減らせます。今...


  • React 入力値の重複チェックをデバウンスして実行する

    ユーザー名の登録フォームでは、入力した値がすでに使用されているかAPIで確認することがあります。入力のたびにAPIを呼び出すとリクエストが増えるため、入力が止まってから重複チェックを実行します。今回は...


  • TypeScript i18nextの動的な翻訳キーを抽出対象に含める

    i18nextでステータスに応じた文言を表示する場合、翻訳キーを動的に切り替えることがあります。ただし、実行時に正しく表示できることと、翻訳キーの抽出ツールが使用箇所を検出できることは別です。今回は、...


  • React useStateで入力フォームを作成する

    Reactでテキストボックスに入力した値を使用するには、useStateで値を保持します。今回は名前とメールアドレスを入力し、送信ボタンを押すと入力内容を表示するフォームを作成します。この例では入力値...


  • React useStateで一覧の追加と削除を行う

    Reactで一覧を表示するときは、配列をuseStateへ保存できます。今回は簡単な買い物リストを作り、項目の追加と削除を行います。配列のpushやspliceで既存のstateを直接変更するのではな...


  • React useRefで入力欄にフォーカスを当てる

    入力フォームを開いた直後や、入力エラーが出たときに、特定の入力欄へフォーカスしたいことがあります。DOMの要素自体を参照するにはuseRefを使います。currentは初回描画中などにnullになり得...


  • React useMemoで重い計算結果を再利用する

    一覧の絞り込みや並べ替えを描画のたびに行うと、データ量や計算内容によっては画面操作が遅くなります。useMemoは、依存する値が変わらない間、以前の計算結果を再利用します。productsまたはque...


  • React useEffectで無限ループが発生するときに確認すること

    ReactのuseEffectを利用したときに無限ループが発生してしまうことがあります。特に注意したいのが、ESLintのreact-hooks/exhaustive-depsで表示された警告をUpd...


  • React useEffectの後片付けでイベント登録を解除する

    画面の幅が変わったときに表示を更新したい場合、ブラウザのresizeイベントを登録できます。登録したままにすると、コンポーネントが不要になったあとも処理が残るため、useEffectの後片付けを用意し...