State Management & Architecture

Lesson 1 of 8

When Local State Isn't Enough

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

For six chapters of this Volume, state has had one home: useState, living inside the component that used it. That got you remarkably far. But every real app eventually hits the same wall — a value that many components, scattered across distant branches of the tree, all need to read and change. The theme. The signed-in user. The shopping cart. A stream of toast notifications. Suddenly useState in one place isn't enough, and the internet is happy to sell you a dozen libraries to fix it. This chapter deliberately reaches for none of them. Before you install a single dependency, the senior skill is knowing what kind of state you actually have — because most of what people cram into a global store doesn't belong there at all. Get the taxonomy right and half your "state management problem" evaporates.

The question this chapter answers

A value is needed in ten places across your app. Before you reach for Redux, Zustand, or Context — how do you decide whether it even is global client state, or whether it belongs somewhere you already have for free?

Core Concepts

The progression that got you here — and where it breaks

You already know the arc. Local state with useState handles anything one component owns. When two siblings need the same value, you lift state up to their nearest common parent and pass it down as props — the pattern from Part II. This is the correct default and it scales further than people admit.

It breaks in one specific way: prop drilling. When the component that owns the state and the component that uses it are many levels apart, every layer in between has to accept the prop and pass it along — even components that don't care about it. Those middle components become couriers for data they never read. Change the shape of the value and you edit six files. That is the pain, and it is worth feeling before you medicate it:

React — Try it Yourself
Loading editor…
Preview

Two levels of forwarding is already tedious; imagine ten, in a real tree. This is the problem every state tool claims to solve. But notice the trap: the fix is not "make theme global." The fix is to stop threading it by hand — which Context does without making it "global" in any dangerous sense. We'll build exactly that next chapter. First, the harder question.

Not all state is the same — a taxonomy

The single most valuable idea in this Part: "state" is not one thing. Lumping it together is why so many codebases end up with a bloated global store that's slow to render and impossible to test. Here is the taxonomy that experienced engineers carry in their heads:

Kind of state Examples Where it belongs
Local UI state Is this dropdown open? Input focus. Hover. useState in the component. Never lift it.
Form state Field values, validation, "dirty" flags The form component (or a form library). Local.
URL / router state Current page, search query, filters, tab, pagination The URL itself. Shareable, bookmarkable, survives refresh.
Server cache state The user's orders, a product list, the profile A server-cache library (React Query / SWR). Not a store.
Global client state Theme, auth session, cart, toasts, feature flags Context or a client-state library. This is the only row that needs one.

Read that last column top to bottom. Four of the five kinds have a home that is not a global store — and they're the four that make up the bulk of a typical app. The amount of true global client state most apps need is astonishingly small. Get everything else into its rightful home and what remains is usually trivial.

Everyday analogy

Think of state like belongings in a house. Local UI state is what you keep in your pocket — with you, used constantly, nobody else's business. Server cache state is a library book: you don't own it, you borrowed a copy and must return it / refresh it when the real one changes. URL state is the address written on the envelope — anyone you hand it to lands in the exact same place. Global client state is the thermostat: one shared setting the whole house genuinely reads. People get into trouble by trying to run the whole house from the thermostat — stuffing pocket items and library books into it because it's the box they know.

Three questions to ask before installing anything

When a value feels like it "needs a store," interrogate it first:

  • Is it server state or client state? If the value originates on a server and you're keeping a local copy — a product list, the user's orders, a profile you fetched — it is a cache, and it has all of caching's real problems: staleness, refetching, loading and error status, deduping requests. A general store (Redux, Zustand) gives you none of that; you'd hand-roll it badly. This is server state, and it belongs in React Query / SWR, which we cover later in this Part. Putting server data in a global store is the most common architecture mistake in React.
  • Is it truly global, or can it be colocated? "Lots of components use it" often means "lots of components under one subtree use it." Move the state to the top of that subtree, not the top of the app. The closer state lives to where it's used, the smaller its blast radius and the easier it is to delete later.
  • How often does it change, and who reads it? A theme that flips twice a session and a cursor position that updates 60 times a second are not the same engineering problem. High-frequency values and widely-read values push you toward different tools (atoms and selectors vs. a single Context). Answer this before choosing, not after your app janks.
Real-world example — chika.ng Academy

The Academy (a Next.js + React app on Drizzle/MySQL) looks like it needs a big store, and needs almost none. The list of courses and a student's progress? Server state — fetched and cached, never in a global store. Which lesson tab you're on, the search filter on the course catalogue? URL state — so a student can paste the link and a teacher lands on the exact same filtered view. The genuinely global client state comes down to two things: the auth session (who's logged in, read all over the app) and the theme. Two small values in Context. Everything else has a better home — and that's typical, not lucky.

The options landscape, at a glance

The rest of this Part details each of these. For now, a map so the categories don't blur together — the mistake is comparing a tool from one row against a tool from another:

Category Representatives Best for
Context (+ useReducer) Built into React Low-frequency global values (theme, auth, locale). No dependency.
Reducer stores Redux Toolkit Large apps, complex update logic, time-travel debugging, big teams wanting one strict pattern.
Minimal stores Zustand Global client state with less ceremony than Redux; fine-grained subscriptions out of the box.
Atomic state Jotai, Recoil State composed of many independent "atoms"; high-frequency, granular updates without re-rendering the world.
Server-cache React Query, SWR Remote data: fetching, caching, refetch, loading/error status. A different category — often used alongside one of the above.

The load-bearing insight: that last row is not competing with the others. A healthy app frequently runs React Query for server state and a little Context or Zustand for the handful of global client values. "Redux vs React Query" is a category error — like asking whether you need a fridge or an oven.

History changed here

Redux arrived in 2015 and, for years, was almost synonymous with "React state." Teams reached for it on day one and dutifully poured everything into it — including server data, which is where the pain lived. Around 2019, Tanner Linsley's React Query reframed the whole conversation with a single observation: most of what people called "global state" was actually server cache, and it deserved a tool built for caching, not a general store. That idea — separate server state from client state — is now mainstream orthodoxy, and it's why modern apps need far less global state than 2016 apps did.

The cost of premature global state

Reaching for a global store on day one isn't neutral — it charges rent forever:

  • Coupling. State in a shared store can be read and written from anywhere, which sounds like a feature and behaves like a liability. You lose local reasoning — the ability to understand a component by reading only that component. Now any part of the app might be mutating the value you're debugging.
  • Re-renders. A naive global value re-renders every consumer on every change. Colocated state re-renders one subtree. The fix is real work (selectors, memoization) that colocated state never needed.
  • Testing. A component with local props is trivial to test — pass props, assert output. A component wired into a global store needs the whole store stood up, seeded, and reset between tests. Multiply that across a suite.
The engineer's notebook

How seasoned engineers actually decide, in order:

• Start local. Always. useState until it genuinely hurts — the pain is the signal, and it's cheap to refactor once you feel it.
• Ask "is this the server's data?" first. If yes, it's a cache — reach for React Query, not a store. This single question prevents most bad architecture.
• Try the URL before a store. Filters, tabs, pagination, the open item — if it should survive a refresh or be shareable, it belongs in the URL.
• Lift to the smallest common ancestor, not the root. "Global" is a last resort, not a first instinct.
• Only then pick a global tool — and pick it for the shape of the few values that are left.

Common Mistakes

  • Putting server data in a global store. Product lists, user profiles, orders — these are a cache, with staleness and refetch problems a store won't solve. Use a server-state library. This is the number-one mistake.
  • Reaching for Redux on day one. Installing heavy global state before you have a global-state problem. Start with useState, lift up, and only escalate when prop drilling genuinely hurts.
  • Ignoring the URL as a store. Rebuilding filters, tabs, and pagination in component state when the URL would give you shareable, refresh-proof, back-button-friendly state for free.
  • Making everything global "just in case." Global by default destroys local reasoning and balloons your re-render surface. Colocate; promote only what's proven shared.
  • Confusing "many components use it" with "it must be global." Often those components share one subtree — lift to that subtree's root, not the app's.
  • Choosing a tool before categorizing the state. Picking Zustand vs Redux vs Context before asking whether the value is even client state. Taxonomy first, tool second.
Thought experiment

Open any app you use daily and mentally list ten pieces of "state" on the screen — the sidebar toggle, the search text, the results, the logged-in avatar, the active tab, the unread count, the theme, the current page, a hovered tooltip, the cart total. Now sort each into the five buckets from the taxonomy. Be honest: how many landed in global client state? For most apps the answer is one or two — theme and session — and everything else is local, form, URL, or server cache. That ratio is the whole thesis of this Part: the state-management problem is mostly a state-classification problem.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
"What is prop drilling, and why is it a problem?"
Model answer — "It's passing a prop down through components that don't use it, just to reach one deep down that does. The problem is the middle components become couriers for data they don't care about — it's verbose, and changing the value's shape means editing every layer in the chain."
Follow-up they'll throw: "How would you fix it?" → Lift state to the nearest common ancestor if the components are close; use Context to skip the intermediate layers if they're far apart. Note that fixing drilling doesn't mean making the value truly global.
Intermediate
"You're fetching a user's orders and showing them in several components. Where does that state live?"
Model answer — "That's server state, not client state — it's a cached copy of data that lives on the server. I wouldn't put it in a global store like Redux, because a store gives me nothing for the real problems: staleness, refetching, loading and error status, deduping requests. I'd use a server-cache library like React Query, keyed by the query, and let every component that needs orders subscribe to that cache."
Follow-up: "When would a global client store still be justified?" → For genuinely client-only, app-wide values: theme, auth session, cart, notifications — things not owned by the server.
Senior
"A team wants to move all app state into Redux for consistency. Do you agree? Design the state architecture."
Model answer — "I'd push back on 'all state, one place.' I'd categorize first: local UI state stays in components; form state stays in the form; anything shareable or refresh-surviving — filters, tabs, pagination — goes in the URL; server data goes in a cache layer like React Query; and only true global client state — session, theme, cart — goes in a client store, which could be Redux or something lighter. 'Consistency' via one giant store is a false economy: it couples everything, inflates re-render surface, and makes components hard to test in isolation. The consistent thing worth enforcing is the decision rule for where state goes, not a single container."
The senior move is refusing the premise and replacing it with a taxonomy plus a decision rule — architecture is about where things belong, not which library wins.
Company lens — how the same topic gets asked
micro1Live and conversational — reason out loud. Expect "walk me through how you'd decide where a piece of state lives." Narrate the taxonomy; don't jump straight to naming a library.
MetaMeta created React — expect first-principles depth. Be ready to explain why React's top-down data flow makes drilling the natural failure mode, and why Context is a delivery mechanism, not a store. They'll probe re-render implications.
GoogleWants clean categorization and trade-offs. Expect "server state vs client state" pressed until you articulate caching concerns precisely — staleness, invalidation, dedupe.
Amazon / AWSOperational lens: "what happens at scale, and what does it cost to maintain?" Tie premature global state to coupling, re-render cost, and testing burden across a large team.
Hands-on exercise
  1. In the editor above, add a second value the deep button needs — say a fontSize — and thread it through Page and Toolbar too. Feel how the drilling doubles. That ache is the motivation for the next chapter.
  2. Take an app you know and list ten pieces of on-screen state. Sort each into the five taxonomy buckets. Count how many are truly global client state — most people are surprised how few.
  3. Pick one filter or tab in that app that currently lives in component state. Argue, out loud (as in a micro1 screen), why moving it to the URL would make it shareable, bookmarkable, and refresh-proof — and what you'd lose.
  4. Write a one-sentence decision rule you could hand a teammate: "Put state in X when ___, in the URL when ___, in a server cache when ___, and in a global store only when ___."

Chapter Summary

  • The progression is useState → lift state up → prop drilling → and drilling is where hand-threaded props break down.
  • Fixing prop drilling does not mean making a value global — it means stopping the manual threading.
  • "State" is five different things: local UI, form, URL/router, server cache, and global client state. Four of the five don't belong in a global store.
  • Ask three questions first: Is it server or client state? Is it truly global or colocatable? How often does it change and who reads it?
  • Server data is a cache (staleness, refetch, loading/error) — it belongs in React Query/SWR, never a general store. This is the most common architecture mistake.
  • The URL is an underused store: filters, tabs, and pagination there are shareable and survive refresh for free.
  • Premature global state charges rent forever: coupling, re-renders, and testing pain. Start local; escalate only when it hurts.
Narrative bridge

We've drawn the whole map: what kinds of state exist, and where each truly belongs. For the small, genuine slice that is global client state — theme, session, and friends — we need a way to deliver it deep into the tree without drilling it through every layer by hand. React ships that mechanism in the box. Next chapter, "Context for State," we build it — and learn precisely when it's the right tool and when it quietly becomes a re-render trap.

Editorial reflection — the industry spent years arguing about which state library wins, and the answer turned out to be a better question: what kind of state is this? Almost every messy React codebase is messy for the same reason — it treated one hard problem as if it were five different ones, or five different problems as if they were one. The engineers who stay calm as apps grow aren't the ones who memorised Redux. They're the ones who, before installing anything, can look at a value and say exactly where it belongs — and, more often than you'd think, discover it belongs nowhere new at all.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.