Component State vs Application State
Date: 2026-08-19
Most “we need a state management library” conversations are misdiagnoses. Sort the state by who needs it and how long it should live, and what’s left over — genuinely shared, genuinely client-owned, genuinely long-lived — is usually small enough not to need one.
Component state belongs to one component and dies with it. Application state outlives any single component and is read by parts of the tree that don’t know about each other.
The useful sort isn’t two buckets, it’s five — and four of them have a correct home that isn’t a store:
KIND EXAMPLE LIVES IN SURVIVES
──── ─────── ──────── ────────
Ephemeral UI dropdown open, hover, the component nothing
input focus
URL state search query, page number, the URL refresh, share,
active filters, sort back button
Server cache products, cart contents, a fetching layer refresh
the logged-in customer keyed by request
Session state auth token, consent choice, cookie / storage refresh, tabs
locale
Genuinely shared theme, feature flags, a store the session
client state an open modal's identity
Where session state physically lives — the fourth row — is Client Storage and Cookies.
The test
Three questions, and the order matters — asking “who needs it” first sends filters to local state, which is where they go wrong:
- Should it survive a refresh, or be linkable? Yes → the URL. Filters, tabs, pagination and search all belong here and are constantly, wrongly, put in memory
- Does the server own the truth? Yes → it’s a cache, not state — see Data Fetching Patterns
- Does anything outside this component need it? No → local state. Most of what’s left stops here
Only what falls through all three is application state.
Why the server-cache confusion is the expensive one
Putting fetched data in a global store looks like state management and is actually cache management wearing its coat. The moment you do it, you own problems the store has no vocabulary for:
- Staleness — how old is this, and when do we refetch?
- Invalidation — which of the nine screens holding this product now need to change?
- Deduplication — three components mounted, one request or three?
- Race conditions — the slower of two in-flight requests lands last and wins — Race Conditions
Hand-rolled, that becomes a permanent maintenance surface: the reducer that grew a loading flag, then an error flag, then a lastFetchedAt, then a manual invalidation call in an unrelated component. A fetching layer keyed by request does all four by construction, which is why route loaders and query caches displaced the global-store-for-server-data pattern.
In plain terms: you don’t own that data. You own a copy of it, and copies go stale.
The URL is underused
Filters and pagination held in memory produce three complaints that always get diagnosed as something else: the back button does nothing, a shared link opens the wrong view, and a refresh loses the user’s place. All three are the same bug. The URL is state storage with free persistence, free sharing and free history — see The History API and URL Structure.
The counter-argument is that it’s stringly typed and needs parsing on every read. True, and worth it for anything a user would expect to be able to send someone.
Lifting state, and how far
The standard move when two components need the same value is to lift it to their nearest common ancestor. It’s correct, and it has a limit: lift too far and every keystroke in a deeply nested input re-renders a page-sized subtree, because the value now lives at the top of it.
Lift to the nearest common ancestor, not to the root. When the nearest common ancestor is the root, that’s the actual signal that this is application state, and State Management Patterns is the next question.