Qadam Roadmap
проектdocs/architecture/tanstack-query.md

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.tsSingleton QueryClient с настройками staleTime и streaming dehydration
apps/web/src/shared/lib/query-provider.tsxClient-компонент с 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 без await
  • shouldRedactErrors: 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 над данными.