The State Decision Tree Ask a team "how do you manage state?" and the answer is usually a tool name — "we use Redux," "we use NgRx," "we use Signals." That's answering the wrong question. The tool is the last decision, not the first. The real question is: at what level does this piece of state belong? Get the level right and the tool almost picks itself. Get it wrong, and no tool saves you — the result is a global store full of things that should have been local, and a re-render storm that takes a quarter to fully debug. The fix isn't a library. It's a decision tree — five levels, climbed only when a concrete need forces it. The five levels Level 1 — Local component state (the default) Does only this component care about this value? A dropdown's open flag, a form field's current text, a hover state. Keep it right there. This is the default, and most state should never leave it. If no other component needs this value, it doesn't belong anywhere else. Level 2 — Lifted state Two sibling components need the same value — a filter panel and the list it filters. Lift the state to their nearest common parent and pass it down. No library needed yet, and none should be reached for. Lifting is free, and it keeps the data flow obvious. Level 3 — Shared via context / service The value is needed by many components across a subtree, and threading it through every intermediate layer has become genuinely painful. Now a shared holder earns its place — React Context, or an Angular service. Two cautions apply: this is for cross-cutting, relatively stable data (the current user, the theme, a feature flag), and context or a service is a distribution mechanism, not a state manager — it moves a value around; it doesn't provide devtools, middleware, or update discipline on its own. Level 4 — Global client store Genuinely app-wide client state, with many independent writers, complex update logic, and a real need for debuggability — time-travel, action logs, predictable mutations. This is where a real store earns its overhead: Redux Toolkit, Zustand, or NgRx. The bar here is deliberately high. A store is powerful, and it is real cost — boilerplate, indirection, a learning curve for every new hire. Climbing here without that cost being earned is the single most common state-management mistake. If the honest reason for reaching for a store is "everyone uses one," that's the signal to climb back down. Level 5 — Server state (a different axis entirely) This is the level most teams miss, and it's the one that matters most. A large share of what ends up crammed into a global store isn't client state at all — it's server state : data that lives in a database, arrives asynchronously, can go stale, and needs caching, refetching, and invalidation. That's a fundamentally different problem from "is the dropdown open," and it deserves a purpose-built tool: TanStack Query, RTK Query, or Angular's resource APIs. The moment , , and are being written into a reducer by hand, stop — that's a server-cache library's entire job, and it already does it better. The insight most teams miss: server state is not client state. Separate them, and most of the "global state" problem disappears — because it was never client state to begin with. In Angular The levels map to concrete tools: Level 1 (local): a inside the component. Level 2 (lifted): state as a signal in the parent, passed via to children. Level 3 (shared): a service holding signals — Angular's DI is the context mechanism. Level 4 (global store): NgRx (or a signal-based store) when actions, effects, and devtools are genuinely needed. Level 5 (server state): a resource API or query wrapper — not a refetched by hand. In React Same tree, different syntax: Level 1 (local): . Level 2 (lifted): move to the common parent, pass value and setter down. Level 3 (shared): plus a provider, for cross-cutting, stable data. Level 4 (global store): Redux Toolkit or Zustand, when many writers and devtools are genuinely needed. Level 5 (server state): TanStack Query — provides loading, error, cache, and invalidation for free. Either way, the tree is identical — only the tool names change. The framework changes the syntax, not the decision. The Trade-off: Is Climbing the Tree Actually Worth the Discipline? Reaching straight for a global store on day one feels like the safe, forward-looking choice — "we'll need it eventually, might as well set it up now." Whether that's true is worth examining honestly. The pain, concretely v1: A new app starts, and a sidebar's open/closed flag goes straight into the global store, "because that's how state works here." One trivial value, one store entry. v2: More simple, single-component values accumulate the same way — a filter string, a selected tab index, a hover flag — each requiring its own action type and reducer case, even though nothing outside its own component ever reads it. v3: A genuinely shared, cross-cutting value — the current locale — gets buried among dozens of trivial, lo…