Frontend state
Most frontend bugs are state bugs: two copies of one truth disagreeing.
The craft is holding each piece of state in exactly one place, at the
lowest level that needs it.
Method
- Classify the state before placing it:
- Server state (data fetched from an API): belongs in a query cache
layer with staleness rules, not copied into local variables that
drift from the server.
- UI state (open panel, selected tab, draft text): belongs in the
component that owns the interaction, lifted only when a real second
consumer appears.
- Shared app state (current user, theme, active workspace): a small
global store, kept small; a store that mirrors half the server is a
second, worse database.
- URL state (page, filters, selected item): belongs in the URL, so
refresh, back, and share all work. If losing it on refresh would
annoy the user, it is URL state.
- Derive, never duplicate. Anything computable from existing state
(counts, filtered lists, validity) is computed at render, memoized only
when measured as hot. The moment a derived value is stored, it can
disagree with its source.
- Make flows one-directional: state flows down as props or context,
changes flow up as events or actions. A child mutating a parent's data
directly, or two components writing one value, is where "sometimes it
doesn't update" is born.
- Handle the async truthfully. Every fetch renders all three of
loading, error, and empty as designed states, not afterthoughts. Show
stale data with a refresh indicator over a spinner wall where the data
allows; block interaction only where acting on stale data would be
wrong.
- Keep effects for synchronization with the outside world (network,
subscriptions, DOM APIs), not for transforming state into other state;
the transform belongs in render. An effect chain that sets state which
triggers another effect is a loop with extra steps.
Litmus tests
- For each piece of state: who owns it, and could a second copy of it
exist anywhere? Every yes is a bug scheduled.
- Refresh mid-flow: does the UI restore sensibly?
- Kill the network in the middle of every mutation: does the UI tell the
truth about what happened?
Boundaries
Framework conventions (React, Vue, Svelte, or the project's chosen store)
override this document's vocabulary; the ownership principles transfer.
1---2name: frontend-state3description: Decide where frontend state lives and how it flows, so UIs stay predictable as they grow. Use when structuring components, adding state, or untangling prop-drilling and sync bugs.4---56# Frontend state78Most frontend bugs are state bugs: two copies of one truth disagreeing.9The craft is holding each piece of state in exactly one place, at the10lowest level that needs it.1112## Method13141. **Classify the state before placing it:**15 - Server state (data fetched from an API): belongs in a query cache16 layer with staleness rules, not copied into local variables that17 drift from the server.18 - UI state (open panel, selected tab, draft text): belongs in the19 component that owns the interaction, lifted only when a real second20 consumer appears.21 - Shared app state (current user, theme, active workspace): a small22 global store, kept small; a store that mirrors half the server is a23 second, worse database.24 - URL state (page, filters, selected item): belongs in the URL, so25 refresh, back, and share all work. If losing it on refresh would26 annoy the user, it is URL state.272. **Derive, never duplicate.** Anything computable from existing state28 (counts, filtered lists, validity) is computed at render, memoized only29 when measured as hot. The moment a derived value is stored, it can30 disagree with its source.313. **Make flows one-directional:** state flows down as props or context,32 changes flow up as events or actions. A child mutating a parent's data33 directly, or two components writing one value, is where "sometimes it34 doesn't update" is born.354. **Handle the async truthfully.** Every fetch renders all three of36 loading, error, and empty as designed states, not afterthoughts. Show37 stale data with a refresh indicator over a spinner wall where the data38 allows; block interaction only where acting on stale data would be39 wrong.405. **Keep effects for synchronization with the outside world** (network,41 subscriptions, DOM APIs), not for transforming state into other state;42 the transform belongs in render. An effect chain that sets state which43 triggers another effect is a loop with extra steps.4445## Litmus tests4647- For each piece of state: who owns it, and could a second copy of it48 exist anywhere? Every yes is a bug scheduled.49- Refresh mid-flow: does the UI restore sensibly?50- Kill the network in the middle of every mutation: does the UI tell the51 truth about what happened?5253## Boundaries5455Framework conventions (React, Vue, Svelte, or the project's chosen store)56override this document's vocabulary; the ownership principles transfer.