state-management

Designs client-side state architecture - what lives where, how it syncs with the server, how conflicts resolve, and how to avoid common pitfalls. Use when designing state for web or mobile apps, debugging state issues, or choosing state management patterns.

State Management

Every UI bug is a state bug. The component rendered the wrong thing because it had the wrong data. The question is always: where should this state live, and how does it stay correct?

Reference: reference/state-patterns.md - State location decision tree, framework comparison, derived state, optimistic updates, state machines, common bugs.

Where State Lives

The Decision Matrix

QuestionIf YES →If NO →
Does the server own this data?Server stateClient state
Does it persist across page refreshes?Server or URL stateComponent state
Is it shared across multiple components?Global/shared stateLocal component state
Should it be bookmarkable/shareable?URL stateNot URL state

State Types

TypeWhereExamplesTool
Server stateDatabase, APIUser profile, orders, productsReact Query, SWR, Apollo, RTK Query
URL stateBrowser URLCurrent page, filters, search query, sort orderRouter, URLSearchParams
Global UI stateMemory (store)Auth status, theme, sidebar open/closedZustand, Redux, Jotai, Context
Local UI stateComponentInput value, dropdown open, hover stateuseState, local state
Form stateComponent/libraryField values, validation, dirty/touchedReact Hook Form, Formik
Derived stateComputedFiltered list, totals, formatted valuesComputed/memo, selectors

The #1 mistake: Putting server state in a global store (Redux, Zustand) and manually keeping it in sync. Use a server state library instead.

Server State

Data that lives on the server and is fetched/cached/synced by the client.

The Pattern

Component needs data
  → Check cache (is it fresh?)
    → YES: render from cache (instant)
    → NO: fetch from server, update cache, render
  → In background: revalidate stale data

Key Concepts

Cache invalidation: When does cached data become stale?

  • Time-based: "refetch after 5 minutes" (staleTime)
  • Event-based: "refetch after mutation" (invalidate on write)
  • Focus-based: "refetch when user returns to tab" (window focus)

Optimistic updates: Show the expected result immediately, rollback if server rejects.

User clicks "Like"
  → Immediately show liked state (optimistic)
  → Send request to server
    → Success: done (UI already correct)
    → Failure: rollback to previous state, show error

Stale-while-revalidate: Show cached (possibly stale) data instantly while fetching fresh data in the background.

Global UI State

State shared across components that doesn't come from the server.

Keep It Small

Most state people put in global stores should be:

  • Server state → Use React Query / SWR (not Redux)
  • URL state → Use the router (not Redux)
  • Local state → Use component state (not Redux)

What actually belongs in a global store:

  • Auth state (logged in user, token)
  • Theme / appearance preferences
  • UI mode (sidebar expanded, notification panel open)
  • Feature flags (if client-evaluated)

State Shape

// GOOD: Normalized, flat, minimal
{
  auth: { userId: "123", token: "abc" },
  ui: { sidebarOpen: false, theme: "dark" }
}

// BAD: Nested, duplicated, over-stored
{
  currentUser: { id: "123", name: "Alice", orders: [...], profile: {...} },
  selectedOrder: { ...duplicate of data already in currentUser.orders },
  filteredOrders: [...derived data that should be computed, not stored]
}

Local Component State

State that belongs to a single component and doesn't need to be shared.

Use local state for:

  • Input field values (before form submission)
  • Toggle states (dropdown open, accordion expanded)
  • Hover/focus states
  • Animation state
  • Temporary UI state (loading spinners, error messages)

Lift state up ONLY when a sibling component needs it. Don't prematurely globalize.

URL State

The URL is state. Treat it as such.

Put in the URL:

  • Current page/route
  • Search queries and filters
  • Sort order
  • Pagination (page number)
  • Selected tab
  • Modal open with ID (/users?edit=123)

Why: Bookmarkable, shareable, browser back button works, survives refresh.

Don't put in the URL:

  • Transient UI state (hover, focus)
  • Large objects or sensitive data
  • State that changes every keystroke (debounce search → URL)

Derived State

State that can be computed from other state. Never store derived state. Compute it.

// BAD: Storing derived state
const [items, setItems] = useState([...]);
const [filteredItems, setFilteredItems] = useState([...]);
// Now you must keep filteredItems in sync with items AND filter criteria

// GOOD: Computing derived state
const [items, setItems] = useState([...]);
const [filter, setFilter] = useState('');
const filteredItems = useMemo(
  () => items.filter(item => item.name.includes(filter)),
  [items, filter]
);

Common Bugs and Fixes

BugCauseFix
Stale data after mutationCache not invalidatedInvalidate/refetch after mutations
UI flickers on navigationShowing loading state for cached dataStale-while-revalidate, keep previous data
State lost on refreshState in memory, not URL or serverPut persistent state in URL or server
Components out of syncSame data copied in multiple placesSingle source of truth, derived state
Infinite re-render loopState update in render, missing depsFix dependency arrays, move effects
Race condition on fetchSlow request returns after fast oneCancel previous request, use latest
Zombie child updatesAsync callback updates unmounted componentCleanup in effect, use abort controller

Anti-Patterns

Anti-PatternProblemFix
Everything in Redux/global storeUnnecessary complexity, over-renderingMatch state type to the right location
Manual server state syncStale data, complex update logicUse server state library (React Query, SWR)
Storing derived stateOut of sync, extra update logicCompute with useMemo / selectors
Prop drilling 8 levels deepFragile, hard to refactorContext for widely-shared state, composition for component trees
Premature global stateComponents coupled to store shapeStart local, lift up only when needed
Mutable state updatesSubtle bugs, stale closures, no re-renderImmutable updates (spread, immer)