# TanStack Query — паттерн работы в проекте

## Паспорт документа

- Статус документа: working reference
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении архитектурных решений, data layer или frontend data patterns
- Область применения: архитектурные и интеграционные справочные документы проекта
- Связанные документы:
  - [Текущее состояние](../project/current-state.md)
  - [Карта API-маршрутов](./api-routes.md)
  - [Платформенный design](../../specs/qadam-platform/design.md)

## Инфраструктура

### Файлы

| Файл | Назначение |
|------|-----------|
| `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

```ts
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 сам доставит данные клиенту.

```tsx
// 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` — данные уже есть в кэше, компонент не подвисает.

```tsx
// '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 выбросят ошибку о несериализуемых аргументах.

### Мутации

```tsx
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?]`

```ts
['catalog', 'items']              // список
['catalog', 'items', slug]        // одна запись
['seller', 'items']
['seller', 'items', id]
['seller', 'leads']
['admin', 'leads']
['admin', 'moderation', 'items']
['me', 'leads']
['auth', 'me']
```

Это позволяет инвалидировать группой:
```ts
queryClient.invalidateQueries({ queryKey: ['seller', 'items'] })
// инвалидирует и список, и отдельные записи
```

---

## Вложенные Server Components

Каждый Server Component может иметь свой `queryClient` и `HydrationBoundary` — это нормально.

```tsx
// PostsPage — prefetch posts
// CommentsServerComponent — prefetch comments независимо
```

Но учти: `await` между компонентами создаёт waterfall на сервере. Если нужен параллелизм — используй [Parallel Routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) в Next.js.

---

## Что НЕ делать

- Не рендери результат `fetchQuery` в Server Component и не передавай его дочерним компонентам — при рефетче на клиенте данные рассинхронизируются.
- Не используй `queryClient.fetchQuery` без обработки ошибок.
- Server Components — только для prefetch, не для ownership над данными.
