Lesson 6 of 26
Local, Lifted, and Global State
Why state should default to the smallest scope that works, what lifting state actually costs, and why 'put everything in global state' fails as an app grows.
Every piece of UI state has to live somewhere, and the question of where is one of the most consequential decisions in a frontend codebase — made dozens of times a day, mostly on autopilot. Getting it wrong doesn't crash anything; it just makes the codebase progressively harder to reason about, one convenient global variable at a time.
The three scopes, and their honest costs
Local state lives inside one component and nowhere else — a form field's current value, whether a dropdown is open, a hover flag. Its entire value proposition is that reasoning about it is trivial: you can look at one component and know everything that reads and writes this piece of state, because nothing outside that component can touch it.
Lifted state moves up to the nearest common ancestor of the components that need to share it, and flows back down as props (with callbacks flowing up to change it). It's still fully traceable — you can find every consumer by reading the props passed down a bounded subtree — but it costs a bit of plumbing, and enough levels of lifting turns into prop drilling, which is the practical ceiling on how far this scales.
Global state lives outside the component tree entirely, in a store any component can subscribe to. It removes the plumbing problem — no props to drill — but at a real cost: any component can now read or write this state, which means understanding a component's actual behavior requires knowing everything else in the app that touches the same slice of the store.
Local state
Cheapest to reason about — bounded to one component. Wrong tool the moment another component needs the same value.
Lifted state
Shared between a known, bounded set of components via props. Scales until the distance between provider and consumer creates real prop-drilling pain.
Global state
Reachable from anywhere — no drilling, but every consumer is now a hidden dependency you have to track down by searching the codebase.
Why "just make it global" fails at scale
The appeal is obvious: no more threading props through five layers of components that don't care about the value, just to reach the one that does. But that convenience is exactly the cost. Once state is global:
- Locality of reasoning breaks down. A component's behavior is no longer fully described by its own props and local state — it might also depend on a global store, and finding every place that mutates the relevant slice means searching the whole codebase, not reading one file.
- Unintended coupling creeps in. Two features that have nothing to do with each other can end up reading the same global key because it was already there and convenient, quietly binding their behavior together.
- Re-render blast radius grows, depending on the store's subscription model. A store that notifies all subscribers on any change (rather than the specific slice that changed) turns one small state update into a much wider re-render than the actual data change justified.
- Testing gets harder. A component that only reads props and local state can be tested by just rendering it with different inputs. A component wired into a global store needs that store mocked, seeded, or wrapped — for every test, of every component that touches it.
None of this makes global state wrong — a currently-logged-in user, a theme preference, an app-wide notification queue are all textbook global state: genuinely needed in many unrelated places, genuinely global in nature. The mistake is reaching for it as a default because it's convenient, rather than because the actual requirement — many, unrelated consumers — is actually true.
The rule of thumb worth defending in an interview
Start local. Lift when a sibling genuinely needs it, and stop lifting the moment the distance between provider and consumer starts to hurt — that's the signal to consider context or a proper store, not a rule to apply preemptively. Go global only when the data is genuinely needed by many, unrelated parts of the tree, not merely "might be useful somewhere else eventually." The interview-strength version of this answer names the actual trade-off (locality and re-render cost vs. plumbing) rather than reciting "local state good, global state bad" as dogma — because sometimes it's the other way round, and the correct answer really does depend on how many consumers there are and how coupled they should be.
What to remember
- Local state is cheapest to reason about because everything that touches it lives in one component; lifting and going global both trade that away for reduced plumbing.
- Lift state to the nearest common ancestor when a small, known set of components needs to share it — that's usually the first move, not a store.
- Global state's real cost is that any component can become a hidden dependency, which erodes local reasoning and can widen re-render scope depending on subscription granularity.
- Reach for global state because many unrelated consumers genuinely need it — not because avoiding prop drilling feels more convenient today.
Check yourself
3 questions · pass 3/3 to unlock Normalizing State Shape
1.A dropdown's open/closed state is currently local to the dropdown component. Someone suggests moving it to a global store 'to keep things consistent.' What's the strongest argument against that?
2.Two sibling components both need to read and update the same filter value. What's the standard first move, before reaching for a global store?
3.Why does 'just put everything in global state' tend to fail as an app scales, beyond being inconvenient to type?
3 left to answer