React TanStack Queryで初期データを複数のキャッシュへ分配する
管理画面の初期表示で、店舗、商品カテゴリ、配送方法など複数のデータが必要になる場合があります。
それぞれのAPIを同時に呼び出す代わりに、初期表示用APIでまとめて取得するとリクエスト数を減らせます。
今回はまとめて取得した結果を、TanStack Queryの個別キャッシュへ分配する方法を紹介します。
初期表示APIの型を定義する
types.tsexport 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.tsexport 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.tsimport { 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へ分配すると、初回のリクエスト数を減らし、その後の更新は必要なデータだけ再取得できます。