TanStack Query — паттерн работы в проекте
Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев
TanStack Query — паттерн работы в проекте
Паспорт документа
- Статус документа: working reference
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении архитектурных решений, data layer или frontend data patterns
- Область применения: архитектурные и интеграционные справочные документы проекта
- Связанные документы:
Инфраструктура
Файлы
| Файл | Назначение |
|---|---|
apps/web/src/shared/lib/get-query-client.ts | Singleton QueryClient с настройками staleTime и streaming dehydration |
apps/web/src/shared/lib/query-provider.tsx | Client-компонент с QueryClientProvider + ReactQueryDevtools |
apps/web/src/app/layout.tsx | Корневой layout, оборачивает в <QueryProvider> |
QueryClient
getQueryClient() использует паттерн isServer:
- Сервер — новый инстанс на каждый запрос (утечки данных между запросами исключены)
- Браузер — singleton, не пересоздаётся при React Suspense
import { getQueryClient } from '@/shared/lib/get-query-client';
Настройки по умолчанию:
staleTime: 60_000— данные считаются свежими 1 минуту, нет лишних рефетчей на клиентеshouldDehydrateQueryвключаетpending-запросы — поддержка streaming безawaitshouldRedactErrors: false— обязательно для Next.js (иначе не работает detection динамических страниц)
Паттерн использования на страницах
Server Component (page.tsx)
Делает prefetch и оборачивает в HydrationBoundary. Не нужен await — streaming сам доставит данные клиенту.
// app/seller/items/page.tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient } from '@/shared/lib/get-query-client';
import { SellerItemsList } from './seller-items-list';
export default function SellerItemsPage() {
const queryClient = getQueryClient();
// без await — данные стримятся к клиенту
queryClient.prefetchQuery({
queryKey: ['seller', 'items'],
queryFn: () => api.seller.getItems(),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<SellerItemsList />
</HydrationBoundary>
);
}
Client Component
Использует useSuspenseQuery — данные уже есть в кэше, компонент не подвисает.
// 'use client'
import { useSuspenseQuery } from '@tanstack/react-query';
import { api } from '@/shared/api';
export function SellerItemsList() {
const { data } = useSuspenseQuery({
queryKey: ['seller', 'items'],
queryFn: () => api.seller.getItems(),
});
return <ul>{data.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
queryFnдолжна быть вызовом функции, а не ссылкой (() => api.seller.getItems(), неapi.seller.getItems). Иначе Next.js Server Actions выбросят ошибку о несериализуемых аргументах.
Мутации
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/shared/api';
export function DeleteItemButton({ id }: { id: string }) {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: () => api.seller.deleteItem(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seller', 'items'] });
},
});
return <button onClick={() => mutate()} disabled={isPending}>Удалить</button>;
}
Query Keys — соглашение
Используем массив-иерархию: [domain, resource, id?]
['catalog', 'items'] // список
['catalog', 'items', slug] // одна запись
['seller', 'items']
['seller', 'items', id]
['seller', 'leads']
['admin', 'leads']
['admin', 'moderation', 'items']
['me', 'leads']
['auth', 'me']
Это позволяет инвалидировать группой:
queryClient.invalidateQueries({ queryKey: ['seller', 'items'] })
// инвалидирует и список, и отдельные записи
Вложенные Server Components
Каждый Server Component может иметь свой queryClient и HydrationBoundary — это нормально.
// PostsPage — prefetch posts
// CommentsServerComponent — prefetch comments независимо
Но учти: await между компонентами создаёт waterfall на сервере. Если нужен параллелизм — используй Parallel Routes в Next.js.
Что НЕ делать
- Не рендери результат
fetchQueryв Server Component и не передавай его дочерним компонентам — при рефетче на клиенте данные рассинхронизируются. - Не используй
queryClient.fetchQueryбез обработки ошибок. - Server Components — только для prefetch, не для ownership над данными.