Управление состоянием¶
Уровень: L2 (детали) | Вверх: ../README.md
Стратегия¶
Управление состоянием разделено на три уровня в зависимости от природы данных:
| Уровень | Инструмент | Данные | Где живёт |
|---|---|---|---|
| Клиентское | Zustand | Аутентификация, UI-состояние конструктора | localStorage + RAM |
| Серверное | TanStack Query | Вещи, образы, справочники, лента | HTTP-кэш + IndexedDB |
| Локальное | useState/useRef | Формы, модальные окна, фильтры | Компонент |
Обоснование выбора¶
Redux Toolkit не использован из-за избыточности: проект имеет только 2 стора с суммарно ~155 строками кода. Бойлерплейт Redux (slices, reducers, actions, Provider) неоправдан для такого объёма. MobX отклонён из-за зависимости от декораторов и большего размера бандла (~16 KB vs ~1 KB у Zustand). Zustand предоставляет минималистичный API: create() → стор готов, useStore(selector) → подписка с гранулярным ре-рендером.
Потоки данных¶
flowchart LR
subgraph Client["Клиент"]
UI["UI компоненты"]
AuthStore["authStore\n(Zustand)"]
BuilderStore["outfitBuilderStore\n(Zustand)"]
QCache["TanStack Query\nкэш"]
IndexedDB["IndexedDB\n(Dexie)"]
LocalStorage["localStorage"]
end
subgraph Server["Сервер"]
API["REST API\n(Kotlin)"]
end
UI -->|"setState"| AuthStore
UI -->|"setSlotItem"| BuilderStore
UI -->|"useQuery"| QCache
AuthStore -->|"setTokens"| LocalStorage
AuthStore -->|"hydrate"| LocalStorage
QCache -->|"queryFn"| API
QCache -->|"offline read"| IndexedDB
API -->|"response"| QCache
API -->|"initialSync"| IndexedDB
IndexedDB -->|"useLocalItems"| QCache
IndexedDB -->|"background refresh"| API

Zustand-сторы¶
authStore¶
Хранит состояние аутентификации: JWT-токены, профиль пользователя, флаг isAuthenticated. Персистентность реализована через прямую запись в localStorage (не через middleware persist), что даёт контроль над процессом гидрации при запуске приложения.
Ключевые операции:
- hydrate() — вызывается один раз в main.tsx, восстанавливает токены и пользователя из localStorage
- setTokens(access, refresh?) — обновляет токены при логине и refresh
- logout() — очищает все данные, включая IndexedDB (через clearLocalData())
outfitBuilderStore¶
Управляет состоянием конструктора образов: набор слотов (slots), выбранные вещи, заголовок, флаг выбора. Не использует persist — состояние существует только во время сессии конструктора.
Ключевые операции:
- initSlots(slots) — инициализирует слоты из справочника
- setSlotItem(slotId, item) — привязывает вещь к слоту
- clearAll() — сбрасывает конструктор
TanStack Query — паттерны¶
Все серверные данные загружаются через TanStack Query с единой конфигурацией queryClient:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 минут
networkMode: 'always', // работа в offline
},
},
});
Ключевые паттерны:
- Offline-first хуки (useLocalItems, useLocalOutfits) читают из IndexedDB мгновенно, фоново обновляют с сервера
- useInfiniteQuery для бесконечной ленты с курсорной пагинацией
- Инвалидация кэша в onSuccess мутаций: queryClient.invalidateQueries(['outfits'])
- networkMode: 'always' позволяет выполнять запросы к IndexedDB даже без сети
-> Полный код stores: zustand-stores.md | Паттерны Query: tanstack-query-patterns.md