- Handbook
- Frontend / Web Engineering
- Frontend patterns (blueprint)
- State management patterns
State management patterns
Purpose: Project-agnostic map of where state lives, how to choose local vs shared vs server state, and how common libraries and patterns fit together.
Guide · Updated · Source
Audience: Teams aligning with Frontend / web engineering body of knowledge and Frontend patterns (blueprint).
Overview
State management is often the hardest problem in UI development because it couples correctness (single source of truth), performance (who rerenders when), and ergonomics (how easy it is to change behavior). The goal is not one global store—it is the smallest correct scope for each piece of data, with clear update paths.
State categories
| Category | Ownership | Lifecycle | Typical home |
|---|---|---|---|
| Local / component | Component instance | Mount → unmount | useState, refs, Svelte $state |
| Shared / application | App or feature module | Session / app lifetime | Context, Pinia, Zustand, Redux |
| Server / remote | Backend + client cache | TTL, invalidation, background refresh | TanStack Query, SWR, Apollo |
| URL / navigation | Browser + router | Survives refresh; shareable | Route params, query strings |
| Form | Form subtree | Until submit or reset | Controlled fields, form libraries |
| Derived / computed | Derived from source state | Recomputed on dependency change | Selectors, useMemo, computed refs |
Ownership rule of thumb: if only one subtree needs it, keep it there; if the server is the authority, treat the client as a cache with a defined invalidation story.
State scope checkpoint
Decide whether each piece of state belongs in the smallest correct ownership category before continuing design.
- State categoriesMap the data to local, shared, server, URL, form, or derived buckets.
- Current stateInventory where the value lives today and who reads or writes it.
- Checkpoint / gateAsk whether this scope is the smallest that stays correct.
- refine or escalateNarrow scope, lift to shared store, or move authority to the server.
- Continue flowProceed with a traceable update path and clear invalidation rules.
State categories
Current state
|
v
Checkpoint / gate
|
+-- no ──► refine or escalate
|
yes
v
Continue flowFlux / Redux-style cycle
Unidirectional data flow: actions describe what happened; the store holds state; views read and dispatch.
Flux unidirectional cycle
Actions describe events; reducers update store state; views render and dispatch the next action.
- Flux / Redux-style cycleFrame every update as a one-way loop from view through store back to view.
- StartUser interaction or an async result becomes a typed action.
- Core steps (see walkthrough below)Reducer applies the action; the store holds the single source of truth.
- OutcomeSubscribed views rerender from the updated store slice.
Flux / Redux-style cycle
Start
|
v
Core steps (see walkthrough below)
|
v
Outcome
Note: When this shines:When this shines: predictable updates, time-travel debugging, large teams needing conventions. Cost: ceremony unless you standardize selectors and slice patterns.
Library comparison matrix
| Library | Paradigm | Boilerplate | Devtools | Learning curve | Bundle (rough) |
|---|---|---|---|---|---|
| Redux | Explicit actions + reducers | High | Excellent | Medium | Small core + ecosystem |
| MobX | Observable / reactive | Low | Good | Medium | Moderate |
| Zustand | Minimal store + selectors | Very low | Add-on | Low | Small |
| Recoil | Atoms + selectors (React) | Medium | Good | Medium | Moderate |
| Jotai | Atomic bottom-up (React) | Low | Add-on | Low–medium | Small |
| Pinia | Stores (Vue) | Low | Good (Vue) | Low | Small |
| NgRx | Redux-like (Angular) | High | Excellent | Steeper | Larger |
| Svelte stores | Writable/readable/derived | Low | Browser | Low | Tiny (framework) |
Bundle sizes vary by version and tree-shaking—treat as relative guidance.
Server state management
| Concern | Practice |
|---|---|
| Cache invalidation | Invalidate by key, tag, or mutation success; prefer stale-while-revalidate for reads. |
| Optimistic updates | Update UI immediately; roll back on error; reconcile with server truth. |
| Background refetch | Refetch on focus, reconnect, or interval for data that goes stale. |
| Deduplication | Same key in flight should share one request. |
| Library | Strength |
|---|---|
| TanStack Query | Normalized patterns, mutations, devtools |
| SWR | Simple API, Vercel ecosystem |
| Apollo Client | GraphQL cache, normalized entities |
Server state request flow
A trigger fetches or mutates remote authority, then hands a reconciled cache outcome to the UI.
- Server state managementTreat the backend as source of truth with a governed client cache layer.
- Actor / triggerMount, focus, mutation, or invalidation tag starts the cycle.
- System stepQuery library fetches, caches, and applies stale-while-revalidate policy.
- Outcome / handoffUI reads cached data while reconciliation tracks server truth.
Server state management
Actor / trigger
|
v
System step
|
v
Outcome / handoffState machines and statecharts
Finite states reduce impossible UI combinations (e.g. loading + error + success at once). XState and similar libraries model states, events, and guards explicitly—ideal for wizards, checkout, and async workflows with retries.
Explicit state machine flow
Model phases as finite states and events so impossible UI combinations cannot render together.
- State machines and statechartsReplace implicit boolean flags with states, events, and guarded transitions.
- StartEnter the initial state defined for the workflow.
- Core steps (see walkthrough below)Events trigger guarded transitions between named states.
- OutcomeTerminal state reflects success, failure, or a reviewable handoff.
State machines and statecharts
Start
|
v
Core steps (see walkthrough below)
|
v
Outcome| Signal to use a machine | Example |
|---|---|
| Mutually exclusive phases | Upload: selecting → uploading → done |
| Retries and backoff | Payment submission |
| Parallel regions | Editor with autosave + collaboration |
Form state
| Style | Description | Trade-off |
|---|---|---|
| Controlled | Value from state; every keystroke updates | Predictable; more rerenders if not batched |
| Uncontrolled | DOM holds value; read on submit | Less React/Vue churn; harder cross-field rules |
| Library | Ecosystem | Notes |
|---|---|---|
| React Hook Form | React | Uncontrolled-friendly; low rerenders |
| Formik | React | Controlled; familiar API |
| VeeValidate | Vue | Schema + composition integration |
| Angular Reactive Forms | Angular | Strong typing with typed forms |
URL as state
| Pattern | Use when |
|---|---|
| Route params | Identity of resource (/users/:id) |
| Query params | Filters, sort, pagination, shareable views |
| Deep linking | Restore full UI from URL; document which keys are stable |
Caveat: sensitive data does not belong in query strings; prefer server session or secure storage patterns per product requirements.
Anti-patterns
| Anti-pattern | Problem | Prefer |
|---|---|---|
| Global state for everything | Coupling; hard testing; unnecessary subscriptions | Colocate; server cache for remote data |
| Global store to avoid drilling | Hides data flow; same as above | Lift, composition, or narrow context |
| Stale closures | Handlers see old state in async/effects | Correct dependency arrays; functional updates |
| Unnecessary rerenders | Wide subscriptions, unstable object identities | Selectors, memoization, split stores |
| Duplicated server truth | Two caches disagree | Single query layer; derive view state |
External references
- TkDodo — Practical React Query — server state, caching, and mental model.
- XState documentation — state machines and statecharts.
- Redux Style Guide — patterns and priorities for Redux-style apps.
Keep project-specific performance budgets in docs/development/ and optimization decisions in docs/adr/, not in this file.