Перейти к основному содержимому

React Query usage

React Query предоставляет удобный способ написания запросов в API через react hooks.

Директории. Наименования.В директории src/api/ создаются папки, название которых должно соответствовать названию домена API. Обычно домен прописывается вначале метода, например, метод GET /catalog/banners:search соответствует домену catalog (строго говоря, домен называется PIM, но для удобства единообразия неймингов на фронте и бэке, примем более часто повторяющееся название домена). Наименование файла должно семантически отражать описываемые в нем методы. Например, файл banners.ts содержит методы для CRUD баннера/ов

Структура

api → apiDomainName → method.ts

В директории apiDomainName также есть папка с типами. Типы описываются согласно предоставляемой бекендерами документации в Stoplight’е.

Docusaurus logo

Все методы реэкспортируются в файлике index.ts. Для удобства импортов в места использования. Аналогично и с типами. Например в types/index.ts будет примерно такой код

export * from './banners.ts';
export * from './brands.ts'

Написание хуков

Здесь не должно быть проблем, все дается просто по аналогии с другими примерами.

Не забывать прописывать все типы: для ответа, для ошибки, для параметров (если есть)

👍

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { CommonResponse } from '@api/common/types';
import { client } from '../index';
import { BrandsParams, Brand, BrandBase, BrandBaseWithId, BrandsImageMutateParams } from './types';

const BRANDS_BASE_URL = 'catalog/brands';

export const useBrands = (data: BrandsParams = {}) =>
useQuery<CommonResponse<Brand[]>, Error>({
queryKey: ['brands', data],
queryFn: () => client(`${BRANDS_BASE_URL}:search`, { data }),
});

Для хуков, использующих useQuery, не забывать прокидывать параметры в ключ queryKey Иначе, мы не получим прелести автоматического рефетча при изменении параметров.

Также, считаю хорошей практикой, прописав параметр BrandsParams, не разворачивать его так, как это сделано в примере ниже. А делать это по месту использования хука. Потому что разные наборы параметров могут быть необходимы в разных условиях (и судя по stoplight'у здесь представлены не все)

💩

export const useProducts = ({
id,
name,
code,
category,
brand,
createdAtFrom,
createdAtTo,
priceFrom,
priceTo,
page,
}: Partial<ProductFilter>, isEnabled = true) =>
useQuery<ProductProp, Error>({
enabled: isEnabled,
queryKey: [
'products',
id,
name,
code,
category,
brand,
createdAtFrom,
createdAtTo,
priceFrom,
priceTo,
page
],
queryFn: () => client('catalog/products:search', {
data: {
filter: {
external_id: [code],
name,
code: id,
category_id: category,
brand_id: brand,
created_at_from: createdAtFrom,
created_at_to: createdAtTo,
price_from: priceFrom,
price_to: priceTo,
},
pagination: {
limit: ITEMS_PER_PRODUCTS_PAGE,
offset: page ? (page - 1)*ITEMS_PER_PRODUCTS_PAGE : 0,
type: 'offset',
},
},
}),
});

Пример использования хука мутации

Определим сам хук

const BASE_URL = 'cms/banners';

export const useCreateProductGroupBanner = () =>
useMutation<BannerCreateResponse, Error, BannerCreateParams>(
data => client(`${BASE_URL}`, { data }));

Добавим хук на страницу.

const createProductGroupBanner = useCreateProductGroupBanner();

И используем метод mutateAsync

const res = await createProductGroupBanner.mutateAsync(data));
// какой-то код с res
setBanner(res.data);

Обратите внимание, константа createProductGroupBanner – это объект. Но здесь мы не будем использовать деструктуризацию, для того, чтобы не заниматься бесконечным переименованием в случае использования других хуков мутации на одной странице. Например

💩

const {
data: apiRegions,
isFetching: isLoadingRegions,
error: regionsError,
} = useRegions();

const {
data: apiPrices,
isFetching: isPricesLoading,
error: pricesError,
} = useDeliveryPrices();

Инвалидация кеша.

После использования хука мутации, а именно удаления/обновления/добавления какой-нибудь сущности, то нужно инвалидировать кеш и переполучить данные либо для конкретной сущности, либо для всего списка сущностей. Можно сделать оптимистичное обновление интерфейса и в случае неудачи откатиться назад, но это немного сложно, и на мой взгляд, для этого проекта избыточно.

Сейчас на проекте встречается 2 способа инвалидации кэша:

Первый. После успешной мутации мы скидываем кеш для конкретных ключей. В примере ниже, кэш станет невалидным для всех ключей, начинающихся с 'brands'. Это действие произойдет автоматически.
export const useCreateBrand = () => {
const queryClient = useQueryClient();

return useMutation<CommonResponse<Brand>, Error, BrandBase>(data => client(BRANDS_BASE_URL, { data }), {
onSuccess: () => queryClient.invalidateQueries(['brands']),
});
};
Второй. Мы вручную переполучим данные. Для этого нам нужно получить метод refetch из хука, который использует под капотом useQuery. А потом этот refetch используем в нужном месте, например, при успехе обновления данных об одном конкретном пользователе

const { data, refetch } = useCustomer(id);
const customer = data?.data;
const updateCustomer = useUpdateCustomer();

const onCustomerUpdate = async (values: FormikValues) => {
if (customer) {
await updateCustomer.mutateAsync(
{
...customer,
birthday: values?.birthday?.toString(),
gender: +values?.gender,
city: values?.city,
comment_internal: values?.comment,
},
);
refetch();
}
}

Какой из двух способов использовать, решать вам. Но помните, что в первом случае инвалидация кеша и приведет к переполучению всех данных, чей ключ прокинут в invalidateQuery(key). И это будет происходить всякий раз при мутации.