Server State Is Not Client State A dropdown's open flag and a list of users fetched from an API often end up in the exact same place — the same store, the same reducers, the same calls. That's the mistake this lesson exists to fix. One of those values is something the user is doing right now, entirely inside the app. The other is a copy of data owned somewhere else, arriving late, capable of going stale the instant it lands. Treating them the same is where a huge share of "state management" pain actually comes from. What makes server state genuinely different Server state has properties client state simply doesn't: It's owned elsewhere. The real source of truth is the database, not the component holding a copy of it. It's stale the moment it arrives. Another user, another tab, another process could have already changed it. It's often needed by more than one consumer at once , which raises a question client state never has to answer: should two components asking for the same data trigger two network requests, or share one? It needs a lifecycle client state doesn't — refetching, background revalidation, invalidation after a related mutation. None of that is optional complexity a team can choose to skip. It's inherent to data that lives somewhere else. The only choice is whether to solve it with a purpose-built tool, or reinvent it — usually badly — inside a general-purpose store. The three jobs a query library does that a reducer won't Caching by key. The same query key returns the same cached result to every component that asks for it — no duplicate network requests just because two unrelated components happened to need the same data. Invalidation. When a mutation changes something on the server, the cache needs to be told which cached queries are now stale, so it refetches instead of quietly serving outdated data. Optimistic updates. The UI updates immediately, assuming the mutation will succeed, and rolls back cleanly if it doesn't — a pattern query libraries model explicitly ( , , ), and one that's genuinely painful to hand-build correctly inside a reducer. In Angular Angular's resource APIs (or a thin query-adapter wrapper) provide the same three jobs — a resource keyed by its request parameters, automatically re-fetched when they change, with loading and error state modeled for you rather than reducer cases written by hand. In React TanStack Query is the direct equivalent — for reads, plus for writes, all keyed the same way shown above. Either way, the shape is identical: a key, a fetcher, and a library that owns caching, invalidation, and loading/error state — not a hand-written reducer pretending to be one. The framework changes the syntax, not the decision. The Trade-off: Is a Query Library Worth Adding, Just for "Fetching Data"? A single inside a or an Angular service method looks like less machinery than adding a whole query library. Whether that's true depends entirely on what the data needs to do after it arrives. The pain, concretely v1: A component fetches a user list on mount and stores it in the global store via a thunk or effect. Works. Ships today. v2: A second, unrelated component needs the same list. It either fires its own duplicate request, or the team wires up manual "has this already been fetched" flags to avoid it — extra state, extra edge cases, for a problem caching would have solved automatically. v3: A mutation — editing a user — succeeds, but the cached list sitting in the store is never told to refetch. The UI shows stale data until a manual page reload, and nobody notices until a support ticket comes in. v4: Someone bolts on a hand-rolled five-minute cache using a timestamp field and a manual staleness check — quietly rebuilding a chunk of what a query library already does, without deduplication, without automatic invalidation, and without the edge cases already worked out. Side-by-side Server data in a hand-written reducer Server data in a query library --- --- --- Two components need the same data Duplicate request, or manual "already fetched" bookkeeping One cached result, shared automatically A mutation changes the underlying data Stale cache lingers until a manual refresh refetches exactly what's now stale Loading / error state Hand-written flags, one set per fetch site Built in — , , , consistent everywhere Rolling back a failed optimistic update Custom logic, easy to get wrong handles it as a first-class case The signal to switch The moment there's a hand-written / / reducer for any server-fetched value — that's the trigger. That's a query library's entire job, already solved, already tested against the edge cases a first attempt won't anticipate. Start simple A single piece of server data, fetched once and never refetched or shared — application config loaded at startup, say — can be fetched inline without pulling in a query library. The discipline earns its cost the moment the data needs refetching, sharing across more than one component, or invalidation after a mutat…