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

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

管理画面の初期表示で、店舗、商品カテゴリ、配送方法など複数のデータが必要になる場合があります。

それぞれのAPIを同時に呼び出す代わりに、初期表示用APIでまとめて取得するとリクエスト数を減らせます。

今回はまとめて取得した結果を、TanStack Queryの個別キャッシュへ分配する方法を紹介します。

単にすべてのAPIを1本へ統合するのではなく、「初期表示では同時に必要だが、その後は別々に更新されるデータ」を対象にします。初回の往復回数を減らしつつ、更新後まで大きなレスポンスを取り直さないことが、この構成を選ぶ理由です。

初期表示APIの型を定義する

types.ts
export type Store = { storeId: string; name: string };
export type Category = { categoryId: string; name: string };
export type ShippingMethod = { shippingMethodId: string; name: string };

export type ManagementBootstrap = {
  stores: Store[];
  categories: Category[];
  shippingMethods: ShippingMethod[];
};

初期表示APIは3種類の一覧を1つのレスポンスで返します。

個別の更新後は、店舗だけ、カテゴリだけという単位でも再取得できるものとします。

query keyを分ける

managementKeys.ts
export const managementKeys = {
  all: ['management'] as const,
  bootstrap: () => [...managementKeys.all, 'bootstrap'] as const,
  stores: () => [...managementKeys.all, 'stores'] as const,
  categories: () => [...managementKeys.all, 'categories'] as const,
  shippingMethods: () => [...managementKeys.all, 'shippingMethods'] as const,
};

初期表示用と個別データ用に別のquery keyを用意します。

取得結果を個別キャッシュへ保存する

useManagementData.ts
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { managementApi } from './managementApi';
import { managementKeys } from './managementKeys';
import type { Category, ShippingMethod, Store } from './types';

type Items<T> = { items: T[] };

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

  const bootstrapQuery = useQuery({
    queryKey: managementKeys.bootstrap(),
    queryFn: async ({ signal }) => {
      const result = await managementApi.bootstrap(signal);

      queryClient.setQueryData<Items<Store>>(
        managementKeys.stores(),
        { items: result.stores },
      );
      queryClient.setQueryData<Items<Category>>(
        managementKeys.categories(),
        { items: result.categories },
      );
      queryClient.setQueryData<Items<ShippingMethod>>(
        managementKeys.shippingMethods(),
        { items: result.shippingMethods },
      );

      return result;
    },
  });

  const storesQuery = useQuery({
    queryKey: managementKeys.stores(),
    queryFn: ({ signal }) => managementApi.findStores(signal),
    enabled: bootstrapQuery.isSuccess,
  });

  const categoriesQuery = useQuery({
    queryKey: managementKeys.categories(),
    queryFn: ({ signal }) => managementApi.findCategories(signal),
    enabled: bootstrapQuery.isSuccess,
  });

  const shippingMethodsQuery = useQuery({
    queryKey: managementKeys.shippingMethods(),
    queryFn: ({ signal }) => managementApi.findShippingMethods(signal),
    enabled: bootstrapQuery.isSuccess,
  });

  const data = storesQuery.data && categoriesQuery.data && shippingMethodsQuery.data
    ? {
      stores: storesQuery.data.items,
      categories: categoriesQuery.data.items,
      shippingMethods: shippingMethodsQuery.data.items,
    }
    : undefined;

  return {
    data,
    error: bootstrapQuery.error,
    isPending: bootstrapQuery.isPending || data === undefined,
  };
};

初期表示APIの結果を受け取った時点で、3つの個別キャッシュへ値を保存します。

個別Queryは初期取得が成功してから有効になります。すでにキャッシュにデータがあるため、画面はその値をすぐ利用できます。

不要な直後の再取得を防ぐ

初期データをsetQueryDataで保存すると、そのデータは現在時刻に更新されたものとして扱われます。

ただし、staleTimeの初期値は0なので、個別Queryが有効になった直後に再取得する場合があります。

直後の再取得が不要なら、個別QueryへstaleTimeを設定します。

staleTimeを設定する
const storesQuery = useQuery({
  queryKey: managementKeys.stores(),
  queryFn: ({ signal }) => managementApi.findStores(signal),
  enabled: bootstrapQuery.isSuccess,
  staleTime: 30_000,
});

30秒以内は初期表示APIのデータを新しいものとして使用します。

個別更新後は必要なキャッシュだけ無効化する

店舗を更新した場合は、店舗一覧だけを再取得できます。

店舗更新後の処理
const updateStoreMutation = useMutation({
  mutationFn: managementApi.updateStore,
  onSuccess: async () => {
    await queryClient.invalidateQueries({
      queryKey: managementKeys.stores(),
    });
  },
});

初期表示APIをもう一度呼び出す必要はありません。

キャッシュの無効化については、以下の記事でも紹介しています。

更新後に必要なキャッシュを再取得する方法

まとめて取得する範囲に注意する

初期表示APIへ含めるデータが増え続けると、レスポンスが大きくなり、変更頻度の異なるデータが強く結び付きます。

以下の条件を満たすデータをまとめると扱いやすくなります。

  • 同じ画面の初期表示で必ず必要になる。
  • データ量が小さい。
  • 同じ権限で取得できる。
  • 個別の再取得APIも用意できる。

初期表示API全体が1つのHTTPレスポンスなら、通常は成功か失敗のどちらかです。

データごとに成功・失敗を返す場合は、個別キャッシュへ保存する条件や、画面に表示するエラーを決める必要があります。

初期取得をまとめつつ個別のquery keyへ分配すると、初回のリクエスト数を減らし、その後の更新は必要なデータだけ再取得できます。


関連記事

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

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


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

    TanStack Queryでデータを更新しても、取得済みの一覧や詳細は自動では書き換わりません。更新後に古いデータが表示されないよう、関連するQueryを無効化して再取得します。今回は商品一覧と商品...


  • 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の後片付けを用意し...