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’е.
Все методы реэкспортируются в файлике index.ts
. Для удобства импортов в места использования. Аналогично и с типами.
Например в types/index.ts
будет примерно такой код
export * from './banners.ts';
export * from './brands.ts'
Написание хуков
Здесь не должно быть проблем, все дается просто по аналогии с другими примерами.
Не забывать прописывать все типы: для ответа, для ошибки, для параметров (если есть)
👍
import { useMutation, useQuery, useQueryClient } from '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,
isLoading: isLoadingRegions,
error: regionsError,
} = useRegions();
const {
data: apiPrices,
isLoading: 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'),
});
};
const { data, refetch } = useCustomer(id);
const customer = data?.data;
const updateCustomer = useUpdateCustomer();
const onCustomerUpdate = async (values: FormikValues) => {
if (customer) {
updateCustomer.mutate(
{
...customer,
birthday: values?.birthday?.toString(),
gender: +values?.gender,
city: values?.city,
comment_internal: values?.comment,
},
{
onSuccess: () => {
refetch();
},
}
);
}
}
Какой из двух способов использовать, решать вам. Но помните, что в первом случае инвалидация кеша и приведет к переполучению всех данных, чей ключ прокинут в invalidateQuery(key)
. И это будет происходить всякий раз при мутации.