Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.3 KiB
Plaintext
121 lines
3.3 KiB
Plaintext
---
|
|
description: Vite + TanStack Router v1 + TanStack Query v5 — routing, loaders, queries, mutations
|
|
globs: apps/web/**/*.{tsx,ts}
|
|
alwaysApply: false
|
|
---
|
|
|
|
# Vite + TanStack Router + Query
|
|
|
|
Фронтенд: **Vite SPA**, не Next.js. Нет Server Components, App Router, `'use client'`.
|
|
|
|
## Структура
|
|
|
|
```
|
|
apps/web/src/
|
|
routes/ # file-based routes (__root.tsx, _auth/, ...)
|
|
queries/ # queryOptions factories + key factories
|
|
lib/ # api-client, queryClient, auth, schemas
|
|
components/ # domain + layout (UI primitives → @cfdm/ui)
|
|
main.tsx
|
|
```
|
|
|
|
## Архитектура
|
|
|
|
- **Router** — маршрутизация, URL state, navigation, loaders
|
|
- **Query** — server state, cache, mutations
|
|
- **Loader** — `queryClient.ensureQueryData()` до рендера → без спиннеров на route data
|
|
- **Компоненты** — UI; данные из Query cache
|
|
|
|
## QueryClient + Router
|
|
|
|
```ts
|
|
// lib/queryClient.ts
|
|
export const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { staleTime: 60_000 } },
|
|
})
|
|
|
|
// lib/router.ts
|
|
export const router = createRouter({
|
|
routeTree,
|
|
context: { queryClient },
|
|
defaultPreload: 'intent',
|
|
})
|
|
|
|
declare module '@tanstack/react-router' {
|
|
interface Register { router: typeof router }
|
|
}
|
|
```
|
|
|
|
## Query definitions
|
|
|
|
- `queryOptions` factories в `queries/`, не inline в компонентах
|
|
- Key factories: `all` → `lists` / `details` → `list(filters)` / `detail(id)`
|
|
|
|
```ts
|
|
export const serviceKeys = {
|
|
all: ['services'] as const,
|
|
list: () => [...serviceKeys.all, 'list'] as const,
|
|
}
|
|
|
|
export const servicesQueryOptions = () =>
|
|
queryOptions({
|
|
queryKey: serviceKeys.list(),
|
|
queryFn: () => api.get('/api/v1/services'),
|
|
})
|
|
```
|
|
|
|
## Loader + component
|
|
|
|
```tsx
|
|
export const Route = createFileRoute('/_auth/services')({
|
|
loader: ({ context: { queryClient } }) =>
|
|
queryClient.ensureQueryData(servicesQueryOptions()),
|
|
component: ServicesPage,
|
|
})
|
|
|
|
function ServicesPage() {
|
|
const { data } = useQuery(servicesQueryOptions()) // из cache loader
|
|
return ...
|
|
}
|
|
```
|
|
|
|
## Search params
|
|
|
|
- Zod + `validateSearch`; доступ через `Route.useSearch()`
|
|
- Search params = source of truth для фильтров/пагинации
|
|
- Передавать в `queryOptions` для query key и fetcher
|
|
|
|
## Mutations
|
|
|
|
```ts
|
|
onSuccess: (newItem) => {
|
|
queryClient.setQueryData(keys.detail(newItem.id), newItem)
|
|
queryClient.invalidateQueries({ queryKey: keys.lists() })
|
|
}
|
|
```
|
|
|
|
- `setQueryData` + `invalidateQueries`, не только invalidate
|
|
- Навигация после create — когда cache уже тёплый
|
|
|
|
## Routing
|
|
|
|
- `createFileRoute` для file-based routes
|
|
- `<Link>` для внутренней навигации, не `<a href>`
|
|
- Pathless layouts: `_auth/` для protected routes
|
|
- Auth guard в `beforeLoad` pathless route
|
|
|
|
## Запреты
|
|
|
|
- `useEffect` для fetch данных — только loader / `useQuery`
|
|
- Inline `queryKey` в компонентах — только factories из `queries/`
|
|
- `useQuery` с позиционными аргументами (v5 — только options object)
|
|
- `window.location` для search params
|
|
|
|
## Prefetch
|
|
|
|
`onMouseEnter` на `<Link>` → `queryClient.prefetchQuery(detailOptions(id))`
|
|
|
|
## DevTools
|
|
|
|
Только в dev: `TanStackRouterDevtools`, `ReactQueryDevtools`
|