Правила Frontend State Management
Назначение
Документ задаёт стандарты управления состоянием в frontend-приложениях Pin: разделение server state (TanStack Query) и client state (Zustand), организация stores, providers, query keys, mutations, формы (TanStack Forms + Zod) и таблицы (TanStack Tables). Применяется ко всем SPA, в первую очередь к pin-admin.
Базовый стек
| Слой | Технология | Версия | Назначение |
|---|---|---|---|
| Server state | TanStack Query (@tanstack/react-query) | актуальная | Кэш, фоновое обновление, optimistic updates, query invalidation |
| Client state | Zustand | актуальная | UI-состояние, селекторы, persistence |
| Forms | TanStack Form (@tanstack/react-form) | актуальная | Управление формами, валидация, submit lifecycle |
| Validation | Zod | актуальная | Schema-driven validation, runtime type guards |
| Tables | TanStack Table (@tanstack/react-table) | актуальная | Server-side sorting/filtering/pagination, column visibility |
| HTTP client | Axios | актуальная | Базовый клиент, interceptors, retry |
| Router | TanStack Router (@tanstack/react-router) | актуальная | File-based routing, search params, loader integration |
Разделение ответственности
| Тип состояния | Хранение | Примеры |
|---|---|---|
| Server state | TanStack Query | Списки каталогов, текущая CatalogSchema, записи, экспорты, отчёты, нотификации |
| User session | Zustand persisted (localStorage) | JWT, текущий tenant, текущий facility, locale, theme |
| Ephemeral UI | Zustand non-persistent | Modal stack, command palette, toast queue, pending changes |
| Form state | TanStack Form (внутри компонента) | Текущие значения, ошибки, dirty, submission state |
| URL state | TanStack Router search params | Фильтры таблиц, сортировка, page/cursor |
Правило: если данные приходят с backend и могут устареть — TanStack Query. Если это локальный UI-флаг — Zustand. Если это контракт с URL (deep linking) — search params.
Организация Zustand stores
src/shared/stores/
auth-store.ts // current user, JWT, expiration, refresh
tenant-store.ts // current TenantId, available tenants
facility-store.ts // current FacilityId, available facilities, switch
permissions-store.ts // effective permissions кэшируются на сессию
theme-store.ts // light/dark/system, persisted
locale-store.ts // ru-RU/en-US, persisted
command-palette-store.ts // Ctrl+K state
modal-stack-store.ts // stack of open modals
notifications-store.ts // toast queue
filter-state-store.ts // ephemeral filter draft до подтверждения
pending-changes-store.ts // несохранённые изменения форм
Правила stores
- Каждый store в отдельном файле.
- Все stores создаются через
create<TState>()((set, get) => ({ ... })). - Persistence через
persistmiddleware с явнымpartialize(не сохранять чувствительные данные кроме JWT). - Selectors на уровне consumers (
useStore((s) => s.field)) — никогда неuseStore()без selector. - Side effects (API запросы) в stores запрещены — store только хранит состояние. Запросы делаются через TanStack Query / mutation; результат пишется в store.
- Каждый store имеет свой
.mdфайл в04-frontends/pin-admin/07-shared/stores/<store>.md.
Шаблон store
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface ThemeState {
theme: "light" | "dark" | "system";
setTheme: (theme: "light" | "dark" | "system") => void;
}
export const useThemeStore = create<ThemeState>()(
persist(
(set) => ({
theme: "system",
setTheme: (theme) => set({ theme }),
}),
{ name: "pin-pin.theme" }
)
);
Организация TanStack Query
Query keys
Каждое entity имеет свой *Keys factory:
// src/entities/catalog-definition/query-keys.ts
export const catalogDefinitionsKeys = {
all: ["pin", "definitions"] as const,
list: (facilityId: string, filters: ListFilters) =>
[...catalogDefinitionsKeys.all, "list", facilityId, filters] as const,
detail: (id: string) =>
[...catalogDefinitionsKeys.all, "detail", id] as const,
schemas: (id: string) =>
[...catalogDefinitionsKeys.all, "schemas", id] as const,
};
Правила query keys
- Иерархия:
[entityArea, action, ...scopes, ...filters]. - Все ключи
as const-литералы. - Никогда не хардкодить ключи в
useQuery({queryKey: [...]})— только через factory. - При мутации инвалидировать максимально точные ключи, не
["pin"].
Settings
// src/shared/lib/query-client.ts
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
gcTime: 5 * 60_000,
retry: (failureCount, error) =>
failureCount < 2 && !isNonRetryableError(error),
refetchOnWindowFocus: import.meta.env.PROD,
},
mutations: {
retry: 0,
},
},
});
Mutations
- Все mutations описывают: optimistic update, disabled/loading state, confirmation modal, invalidated query keys, rollback UI, backend error mapping.
- Mutation error mapping переводит
{ code, fieldPath }от backend вsetFieldErrorформы. - После успеха —
queryClient.invalidateQueries({ queryKey: ..., exact: false })на затронутые ключи.
Организация TanStack Forms
Принципы
- Каждая форма — отдельный компонент с
useFormхук. - Zod schema лежит в
src/entities/<entity>/schemas.tsилиsrc/features/<feature>/schema.ts. - Schema используется одновременно для type inference (
z.infer<typeof schema>) и runtime валидации. - Server-side ошибки маппятся на field paths через единый mapper
mapServerErrorToFormFields. - Submission state (idle/submitting/success/error) — внутри useForm, не Zustand.
Шаблон формы
const form = useForm({
defaultValues: { displayName: "", iconExternalFileId: null },
validators: { onChange: createCatalogDefinitionSchema },
onSubmit: async ({ value }) => {
try {
await createMutation.mutateAsync(value);
} catch (e) {
mapServerErrorToFormFields(e, form);
}
},
});
Организация TanStack Tables
Принципы
- Server-side sorting, filtering, pagination — обязательно для любых списков с потенциально >100 строк.
- Column visibility и density сохраняются в Zustand store на пользователя+таблицу.
- Виртуализация (
@tanstack/react-virtual) обязательна для таблиц >100 строк. - Адаптивно: <900px — переход на cards view (см.
ui-responsive-design-rules.md).
Шаблон таблицы
const table = useReactTable({
data: query.data?.items ?? [],
columns,
state: { sorting, columnFilters, pagination, columnVisibility },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
manualPagination: true,
manualSorting: true,
manualFiltering: true,
pageCount: query.data?.totalPages ?? -1,
});
TanStack Router
Файловая структура
src/pages/
__root.tsx // layout root
index.tsx // /
auth/
login.tsx
pin/
index.tsx // /pin
$catalogId.tsx // /pin/:catalogId
$catalogId/
records.tsx // /pin/:catalogId/records
records/$entryId.tsx // /pin/:catalogId/records/:entryId
schema.tsx
Правила
- File-based routing с code-splitting по умолчанию.
- Search params валидируются Zod (
validateSearch). - Loader используется только для критичного prefetch; обычно данные грузятся через TanStack Query внутри компонента.
- Guards (auth, role) — на уровне
__root.tsxи при необходимости на конкретных route'ах черезbeforeLoad.
Axios client
// src/shared/lib/api-client.ts
import axios from "axios";
import { useAuthStore } from "@/shared/stores/auth-store";
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30_000,
});
apiClient.interceptors.request.use((config) => {
const { jwt } = useAuthStore.getState();
if (jwt) config.headers.Authorization = `Bearer ${jwt}`;
config.headers["X-Correlation-Id"] = generateCorrelationId();
return config;
});
apiClient.interceptors.response.use(
(r) => r,
async (error) => {
if (error.response?.status === 401) {
await useAuthStore.getState().refresh();
return apiClient.request(error.config);
}
return Promise.reject(mapAxiosError(error));
}
);
Правила
- Один Axios instance на приложение.
- Interceptors:
- request: добавление
Authorizationheader,X-Correlation-Id. - response: маппинг ошибок в типизированную
ApiError, refresh-токен на 401.
- request: добавление
- Никаких глобальных fetch wrappers поверх Axios — только Axios.
Validation с Zod
- Все request DTOs описаны Zod-схемами.
- Все response DTOs (для критичных endpoints) валидируются Zod на boundary API adapter-а.
- Schema именуются как
<purpose>Schema:createCatalogDefinitionSchema,updateFacilitySchema. - Reuse через
.pick(),.omit(),.extend().
Запрещено
- Redux, MobX, Recoil в новом коде.
useContextдля хранения состояния, которое можно положить в Zustand или TanStack Query.useEffectс фетчем данных — только TanStack Query.- Хранение JWT в
sessionStorageили cookies безhttpOnly(если cookie —httpOnly,SameSite=Strict,Secure). - Импорт
react-query(старая версия) — только@tanstack/react-query.
Обратные ссылки
Автоссылки: где используется этот документ.
- frontend-design-system-rules.md (1): Правила Frontend Design System
- frontend-form-field-behavior-rules.md (1): Правила Поведения Полей В Формах-Редакторах
- frontend-fsd-and-atomic-rules.md (1): Правила Frontend FSD И Atomic Design
- frontend-store-template.md (1):
Связанные документы
- frontend-rules.md
- frontend-design-system-rules.md
- frontend-fsd-and-atomic-rules.md
- frontend-documentation-rules.md
docs/04-frontends/pin-admin/00-conventions/state-management-conventions.md— конкретика для приложения.docs/04-frontends/pin-admin/07-shared/stores/— каждый store отдельным md.