System Design Cases
State Management: server, client, URL, form state
Frontend State Management concept page covering server state (TanStack Query), client/UI state (Zustand), URL state (Next.js searchParams), and form state (React Hook Form). Includes 2 ADRs (Zustand vs Redux Toolkit vs Jotai for 2026; TanStack Query vs SWR vs custom useEffect) and 3 scenarios: local state lifted to Zustand, server state cache+refetch+mutation+invalidation, URL state filters with TanStack cache reuse.
Frontend state management: ownership, snapshots и server state
State management начинается не с выбора библиотеки, а с определения authority и lifetime. Локальный UI state, shared client state, URL state и кэш server-owned data имеют разные правила обновления, persistence и invalidation.
React рассматривает state текущего render как snapshot и может batch обновления. Асинхронный ответ поэтому обязан проверять identity/version запроса, а derived data лучше вычислять из канонического источника, не хранить второй независимой копией.
Проверяемые утверждения
- State следует держать как можно ближе к общему владельцу, но не ниже места, где его должны согласованно читать и изменять несколько компонентов.
- Updater, зависящий от предыдущего значения, должен вычисляться из фактического previous state; захваченный render snapshot не обновляется внутри уже выполняющегося handler.
- Server state остаётся удалённым и может измениться без клиента. Query cache хранит снимок с freshness/invalidation policy, но не превращается в источник бизнес-истины.
- External store обязан давать согласованный snapshot и корректную подписку. Hydration требует одинакового server/client initial snapshot либо явного client-only rendering.
Границы и компоненты
| Компонент | Роль в модели |
|---|---|
| User and Route Events | local intent; shared command |
| Local UI State | local intent; read derivation; query subscription |
| Shared Client Store | shared command; read snapshot; explicit persisted subset |
| Pure Derived Selector | read derivation; read snapshot |
| Versioned Query Cache | query subscription; fetch and mutate |
| Authoritative Data API | fetch and mutate |
| Scoped Client Persistence | explicit persisted subset |
Сценарии
Keep transient UI state local
A disclosure toggle belongs to one component subtree. The event updates local state with a functional updater and does not enter the global store or durable persistence.
Проверяемый исход: The state resets with the intended component identity and cannot leak into another account or route.
Derive a shared view from one source
A shared command replaces normalized entities immutably. Components read a memoizable selector instead of maintaining a second writable total or filtered list.
Проверяемый исход: One canonical client snapshot produces all derived views; no duplicated field can drift after a batched update.
Reconcile an optimistic mutation
The query layer records an operation identity, renders an optimistic projection, sends the mutation, then replaces or invalidates the projection from the authoritative result.
Проверяемый исход: Success, rejection and retry all converge on a server-confirmed version; an optimistic view is never described as a committed server fact.
Discard an out-of-order response
A route or search key changes while an older request is in flight. The query cache compares the response identity with the active key before publishing it.
Проверяемый исход: A late response can populate its own keyed cache entry but cannot overwrite the currently selected resource.
Failure, concurrency и evolution checklist
- Represent pending, success, empty, rejected, cancelled and stale states explicitly; a single nullable data field cannot distinguish them safely.
- Cancel obsolete work when possible and always guard publication by request key/generation because cancellation can race with completion.
- Version persisted state and migrate or discard incompatible records. Never assume a new bundle can read every old local schema.
- On mutation conflict, re-fetch and re-evaluate intent or present a conflict; blind last-response-wins can erase a newer user action.
Security и privacy
- Do not persist access tokens or sensitive cross-user data merely because a store supports persistence. Partition by authenticated subject and clear on logout/account switch.
- Treat client state as attacker-controlled input at every API boundary. Hidden buttons and store flags are not authorization.
- Redact secrets and personal data from devtools, action logs, replay telemetry and serialized hydration payloads.
Метрики, формулы и допущения
cache_bytes ≈ live_entries × mean_serialized_bytes + index_and_runtime_overhead; measure the overhead rather than assuming the serialized size is total memory.- A normalized indexed lookup can be expected O(1) on average for a hash map; a selector scanning
nrecords is O(n). Memoization changes repeated-work cost only when its identity assumptions hold. staleness_age = observation_time − authoritative_version_time; it is different from request latency and requires comparable clocks or a server-issued timestamp/version.- Optimistic success rate, rollback rate and stale-response discard rate are separate ratios with explicit denominators.
Числа и bounds выше действуют только при названных units, population и assumptions. Ни паттерн, ни browser API сами по себе не задают SLA, capacity или correctness.
Решения для production review
- Classify state by owner, lifetime, share scope, URL representation, persistence and conflict policy before selecting an implementation.
- Keep canonical state minimal; derive totals, filters and presentation labels with pure selectors.
- Use a query/cache abstraction for remotely owned data and reserve a shared client store for genuinely client-owned cross-component coordination.
Первичные и официальные источники
- https://react.dev/learn/managing-state
- https://react.dev/learn/queueing-a-series-of-state-updates
- https://react.dev/reference/react/useSyncExternalStore
- https://tanstack.com/query/latest/docs/framework/react/overview
Scope note
Диаграмма использует React terminology для snapshot/batching, но не объявляет Redux, Context, Zustand или query library универсальным выбором и не моделирует конкретный framework scheduler.