Who Owns This State?
A five-criterion decision — shareable, local-only, server-authoritative, distant, or must-survive-reload — that resolves most state placement questions before a library is chosen.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
Where should this particular value live, and what decides that?
An engineer is adding a feature and has a value in their hands — a selected tab, a search term, a list of results, a draft, a preference. They need to put it somewhere, and the choice will be load-bearing for years.
Put it wherever it is easiest to read from. If two components need it, lift it up. If lifting it gets awkward, put it in the app-wide store — that always works.
"That always works" is true, which is the problem: the store absorbs every value that was inconvenient to place, and becomes a bag of unrelated things with no ownership story at all.
- "That always works" is true, which is the problem: the store absorbs every value that was inconvenient to place, and becomes a bag of unrelated things with no ownership story at all.
- Lifting by convenience moves state upward until it lives above the component that owns it, at which point every sibling re-renders on a change none of them read (What a Component Costs to Render).
- Values that should have been in the URL never get there, because the store was closer to hand. The application ends up with exactly one addressable page and a Back button that exits it.
- Server data placed by convenience never acquires a freshness policy, because a store has no concept of stale (Stale-While-Revalidate).
- A value that must survive a reload was placed in memory, so a tab discard under memory pressure loses it silently and the user finds out by re-doing the work (The Multi-Process Browser).
What is actually happening
In the browser, not in the framework.
- Placement is a function of five properties of the value, checked in order. The order matters because the earlier criteria are cheaper and more reversible than the later ones.
- Shareable or bookmarkable? If a colleague should be able to open this exact view from a link, it is URL state. The browser then does history, restoration and Back for free (The URL Is Application State).
- Only needed here? If no component outside this subtree reads it, it is component state. Locality is not aesthetic — it is what lets two instances coexist and what bounds the re-render.
- Authoritative on the server? If the server can change it without asking you, the client holds a cache, not a value. It needs a key and an invalidation rule (Server State Is Not Your State).
- Needed by distant, unrelated UI? Then it is shared client state, and the mechanism — props, context, an injected service, a store — is a separate decision about reach and update frequency (Prop Drilling, Context and Global State).
- Must survive a reload? Then it is browser storage or the server, and you must pick which: storage is per-device and per-browser, the server is per-account and everywhere (Persistent Client State).
- A value can match more than one criterion. When it does, the first match is where the value lives, and everything else *derives* from it rather than copying it (Derived State).
What this makes the browser do
And which of it is avoidable.
- Component state costs the smallest re-render the framework can express, plus its downstream style and layout invalidation.
- Shared client state costs a subscription per consumer and, in coarse-grained frameworks, a re-render of every consumer on every change unless a selector narrows it.
- URL state costs a history entry and a route match; the browser also serialises it into the session history, which is why it survives a tab discard and a store does not (History and Navigation).
- Server-owned state costs network work whose amount depends entirely on the freshness policy you chose, not on the placement (The Life of a Fetch).
- Storage-backed state costs a read on startup — synchronous and main-thread-blocking for Web Storage, asynchronous for IndexedDB — which is a real cost at boot (Choosing Browser Storage).
The five criteria, in order
This is not a taxonomy to memorise; it is a sequence to run. Each question is cheap to answer and the first "yes" wins. When two would both be true, the earlier one is the owner and the later one derives.
The order is deliberate. Shareability is first because it is the criterion teams forget entirely and the only one the browser can help with. Locality is second because it is the cheapest correct answer. Server authority is third because getting it wrong produces the most convincing-looking bugs. Distance and persistence are last because they are the two answers that add machinery.
Who is authoritative for this value, and how far does it need to travel?
when Someone should be able to share, bookmark, reload or Back-button into this exact view: filters, tab selection, sort order, page number, selected item id.
cost Serialisation and parsing of untrusted strings, a history-entry policy (push versus replace), a length limit, and permanent exposure in logs, referrers and shared links.
when Nothing outside this component and its children reads it: an open panel, a hover target, an in-progress drag, a locally focused index.
cost It vanishes on unmount and on reload, and a second consumer later requires a small refactor to lift or share it.
when The server is authoritative and other actors can change the value while you hold it: lists, records, anything fetched.
cost A cache key design, a staleness policy, invalidation on mutation, and a background-refetch story that must not fight the user's draft.
when Genuinely distant, unrelated parts of the UI read or write it and no server owns it: theme, locale, a command palette's open state, a multi-step wizard spanning routes.
cost Subscription and re-render management, a testing surface that now needs a provider, and the standing risk that it becomes the default home for everything (Prop Drilling, Context and Global State).
when It must survive a reload or a tab discard. Storage if it is a per-device convenience; the server if the user would expect it on another device.
cost Storage: versioning, migration, quota, per-device divergence and exposure to same-origin script. Server: a request per change and an offline story (The Offline Mutation Queue).
Running the criteria on one screen
Take a support-ticket queue: a filter chip row, a sort control, a paginated list, a detail panel, an inline edit form, a "recently viewed" strip and a density toggle. Seven values, five destinations, and not one of them requires a decision harder than the question above.
What the diagram shows is that the URL is the hub. Filter, sort, page and selection all live there, so the fetch key is a pure function of the URL, the detail panel is a pure function of the selection, and the Back button restores all four at once without any code of yours running.
The same value, five contexts
The criteria are not a lookup table keyed by variable name. "Selected item" is component state in a combobox, URL state in a master-detail list, server state in a collaborative editor and persistent state in a "resume where you left off" feature. The value is identical; the ownership answer is not.
This is why the decision has to be run per feature rather than standardised per data type. A team rule that says "selections go in the store" will be wrong in four of these five columns.
| The value | Context | Owner | Why that one | What breaks if you pick the store instead |
|---|---|---|---|---|
| Selected item | Combobox highlight | Component state | Nothing outside the combobox reads it | Two comboboxes on one page highlight together |
| Selected item | Master–detail list | URL | A link should open that record | The record cannot be shared and Back leaves the page |
| Selected item | Collaborative editor | Server | Other users must see the selection | Each client believes its own selection is universal |
| Search term | Type-ahead inside a menu | Component state | It is transient and per-instance | Every menu on the page filters together |
| Search term | Results page | URL | Bookmarkable, shareable, restorable | Results are unlinkable and the Back button skips the search |
| Theme | Whole application | Shared client state + storage | Read everywhere, must survive reload, per-device | Nothing — this is the shape a store is actually for |
| Draft edit | Any form | Form state, then the server | The user is authoritative until submit | A refetch overwrites the draft mid-keystroke |
How to build it
Most important first.
- Run the criteria in order, out loud, in the pull request. The answer is short and the reasoning is the artifact worth keeping.
- Start at the narrowest placement that works and widen only when a concrete second consumer appears. Widening is a small refactor; narrowing after a year is not.
- Write the answer down next to the value — a one-line comment naming the owner and why — because the next engineer will otherwise re-derive it wrongly under time pressure.
- Give every server-owned value a key and an invalidation rule at the moment you place it, not later (Query Keys and Invalidation).
- When a value is genuinely needed by distant UI, choose the *reach* mechanism separately: prop passing for two levels, context or an injected service for a subtree, a store for genuinely application-wide values with many writers (Prop Drilling, Context and Global State).
- Prefer one owner plus derivations over two owners plus synchronisation, always (Derived State).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Placement decides whether Back is an undo. State in the URL means a keyboard or screen-reader user who navigates into a detail view can leave it with the browser's own control, which is always available and always labelled (History and Navigation).
- Placement decides focus survival. If a value that drives a list lives above the component and its change remounts the subtree, focus is destroyed on every update and a keyboard user is thrown back to the document (Focus Management).
- State that must survive a reload but does not is an accessibility problem specifically for users who take longer to complete a form — an assistive-technology user, or anyone using switch access — because they are the most likely to hit a session or tab boundary mid-task.
- Whatever the placement, the *change* still needs an announcement. Moving a filter into the URL does not announce the new result count; a live region does (Live Regions and Announcement).
What can go wrong
- The criteria applied once, at design time, and never revisited when the feature changed. A value that became shareable stays in memory because that is where it started.
- Answering "must survive a reload" with storage when the honest answer was the server: the user's draft survives on their laptop and is invisible on their phone.
- The mitigation failing: state placed correctly, then read through a convenience singleton that reintroduces global coupling without appearing in any diagram.
- URL chosen for something high-frequency — a scroll position, a slider being dragged — flooding the history stack and making Back useless (The URL Is Application State).
- Shared client state chosen because two components needed it, when the real relationship was that one derived from the other.
- Two placements for one concept — a store value and a URL param — updated by different code paths, diverging the first time one path is skipped.
- A shared client value written by two subtrees in the same tick; the framework batches, and the loser is whichever write the scheduler ordered last (The Event Loop, Precisely).
- A storage-backed value written by another tab between your read and your write (Persistent Client State).
- URL state is published state: history, referrers, server logs, screenshots, shared links. Choose it for filters and ids, never for anything sensitive (The URL Is Application State).
- Storage-backed state is same-origin readable by every script on the page, including third-party ones you added for analytics (Third-Party Scripts and the Supply Chain).
- Placing an authorization claim anywhere on the client is a rendering choice, never an enforcement one. The server re-checks regardless (What the Frontend Is Responsible For in Auth).
- The browser enforces none of these placements. It enforces the origin boundary around all of them, which is a different guarantee (Origins and the Sandbox).
- "The criteria say never use a global store." They say a store is the answer for one specific shape: values genuinely needed by distant, unrelated parts of the UI with many writers. That shape exists; it is just far rarer than the default suggests (Prop Drilling, Context and Global State).
- "Lifting state up is the answer to sharing." Lifting is the answer to *nearby* sharing. Lifted far enough it becomes a global with extra steps and worse re-render behaviour.
- "If it needs to survive a reload, persist it." Only if the user would expect it on that device only. Anything they would expect on a different device belongs on the server.
- "URL state is only for routes." Search parameters are state, and filters, tabs, sort order and pagination live there well (URL Parameters).
- "Ownership is an architecture concern, so it can wait." It is the cheapest decision in the feature and the most expensive one to change afterwards.
Measuring it, and what changes in the field
- Copy the URL after a meaningful interaction and open it in a new window. Whatever does not come back is state you chose not to make addressable — deliberately or not.
- Hard-reload mid-task. Whatever is lost is state that had no persistence answer.
- Open the same view in two tabs and change something in one. Whatever silently diverges is state whose ownership is ambiguous (Auth Across Tabs).
- Framework devtools: a store whose subscribers outnumber its meaningful writers is state that was widened for convenience (Debugging State).
- On a memory-constrained device, tabs are discarded and only URL state and persisted state return (The Multi-Process Browser).
- On a slow network, placement determines whether a reload restores the view instantly from the URL or waits for a fetch to rediscover it.
- With a large dataset, shared client state re-renders become measurable and selectors stop being optional (List Virtualization).
- In a long-lived tab, server-owned state placed as if it were client-owned drifts arbitrarily far from the truth (Long-Lived Clients and Version Skew).
- Following the criteria produces more placements than one store does, and therefore more places to look. The compensation is that each place has a stated owner, so "where does this come from" has an answer.
- URL state costs encoding, decoding and validation of untrusted strings — a user can type anything into a query parameter, and your code must survive it (URL Parameters).
- Keeping state narrow means occasionally passing a value down two or three levels. That is a real cost, and it is smaller than the cost of a global that no longer has an owner.
- Server-as-persistence means a request on every change and a story for offline. Storage-as-persistence means a per-device answer and a migration problem.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe five criteria depend on browser facts — the address bar, the history stack, the network, the storage APIs and the tab lifecycle — so they hold in any framework and in none. Only the mechanism for each answer changes.
- FRAMEWORK-SPECIFICThe "distant UI" answer is where frameworks genuinely diverge. React reaches for context or an external store and re-runs every consuming component unless a selector narrows the subscription; Vue offers provide/inject over refs whose readers are tracked individually; Angular's idiomatic answer is a root-provided injectable service holding signals, which is dependency injection rather than a store; Svelte 5 exports a
$staterune from a module and Solid exports a signal or store, both of which update only the bindings that read them. Advice tuned to React's re-render granularity over-warns about shared state in the fine-grained frameworks.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — this is single-ownership of mutable data restated for a browser, where the candidate owners include an address bar and a cookie jar as well as objects.
- — Distributed Systems — "authoritative on the server" is the client half of a single-writer model; when the client also writes, you have chosen multi-master replication whether or not you called it that.