Chapter 12
The Four Kinds of State
Every value in a React SPA belongs to one of four kinds of state — local, global, server cache, or URL. A decision flowchart that ends the 'which library should I use' debate.
Published 2026-05-23
🎯 Chapter Goal — After this chapter you can name the four kinds of state every SPA has, recognise which one you’re holding when you ask “where should this
useStatelive”, and pick a tool that fits the kind — not the loudest framework that week.🧭 Prerequisites — Ch 2 (hooks), Ch 5 (routing — URL is one of the four).
🔹 12.1 The taxonomy
┌──────────────────────────────────────┐
│ Where does this value live? │
└──────────────────────────────────────┘
│
┌──────────────────┬───────────┴───────────┬──────────────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐
│ LOCAL │ │ GLOBAL │ │ SERVER │ │ URL │
│ │ │ │ │ CACHE │ │ │
│ useState│ │ Zustand │ │ TanStack │ │ search- │
│ useReducer │ Jotai │ │ Query │ │ params │
│ │ │ Recoil │ │ SWR │ │ pathname │
│ │ │ Redux │ │ RTK Query│ │ │
└─────────┘ └─────────┘ └──────────┘ └──────────┘
Local state
Single component owns it. Form input, toggle, hover flag. useState / useReducer cover 100 % of the cases. Anything more is overengineering.
Global state
Two or more unrelated components need the same value. Shopping cart, user profile, feature flags loaded at boot, UI chrome state (sidebar open?).
The disqualifying test: if the value comes from the server and could change behind your back, it’s not global state — it’s server cache.
Server cache
Data your backend owns, fetched over HTTP. Lists of invoices, the current user record, autocomplete results. The cache concerns: when is it stale? when do we re-fetch? how do mutations invalidate? how do two components asking for the same thing coordinate?
Tools: TanStack Query (the book’s default — Ch 14), SWR, RTK Query, Apollo if you’re on GraphQL.
URL state
State that should be bookmarkable / shareable / survives reload. Filter selections, multi-step wizard position, current dashboard view. Lives in the URL pathname or search params.
Tools: native URLSearchParams, nuqs, TanStack Router’s validateSearch (Ch 5 §5.2, Ch 15).
🔹 12.2 The decision flowchart
Does the value come from the server (HTTP/WS)?
│
├── Yes ──▶ Server cache (Ch 14)
│
└── No
│
Does it need to survive reload or be shareable?
│
├── Yes ──▶ URL state (Ch 15)
│
└── No
│
Does more than one unrelated component need to read it?
│
├── Yes ──▶ Global state (Ch 13)
│
└── No ──▶ Local state (useState / useReducer)
Run the flow before you decide. Most “I need Redux for this” instincts are wrong; usually you need server cache or URL state.
🔹 12.3 Picking the wrong kind — the cost
Wrong: storing server data in global state.
Symptoms: stale data, manual revalidation logic in components, refresh button everywhere. Cost: 200–500 lines of cache-invalidation code that TanStack Query would handle for free.
Wrong: storing URL-worthy state in local state.
Symptoms: refresh resets filters; “can you share this view?” → “no, you’ll have to apply the filters again.” Cost: every user has to re-apply selections; bookmarks don’t work; deep links into product UIs are impossible.
Wrong: storing local state globally.
Symptoms: a Zustand store named formInputStore with setEmailFieldValue, setPasswordFieldValue. Cost: re-render storms across the app on every keystroke; obscures the form’s natural locality.
🔹 12.4 The “Context as global state” trap
This is the most common bug pattern in 2026 React. People reach for useContext because it’s built in.
const AppContext = createContext<AppState>(/* … */);
// Top-level provider that holds half the app's state:
<AppContext value={{ user, theme, cart, notifications, filters }}>
{children}
</AppContext>
The trap: every consumer of AppContext re-renders whenever any field changes. Update a notification, every component reading user re-renders. Update the cart, every component reading theme re-renders.
The fix depends on what you really have:
- Server data (user, notifications) → TanStack Query.
- Cross-component UI state (theme, sidebar-open) → Zustand (selectors avoid the re-render storm).
- Form/filter state — usually URL state.
useContext is the right tool only when the value rarely changes (locale, a stable feature-flag set) or when consumers are explicitly co-located in a subtree (FormProvider).
🔹 12.5 Concrete examples — “which kind?”
| Value | Kind | Why |
|---|---|---|
email field on a login form | Local | Single component, transient |
isMenuOpen for a nav drawer | Local or Global | Local if only the nav reads; global if the page also needs it |
| List of invoices | Server cache | Backend owns it; can change |
| Current logged-in user | Server cache | Even though “global”-feeling, it’s data from the server |
| Theme (light/dark/system) | Global | Multiple components react; no server involvement |
| Selected invoice filters | URL | Bookmarkable view |
| Pagination page number | URL | Bookmarkable; survives reload |
| Modal open/closed | Local | UI state of one parent |
| Toast notifications | Global | Any component can fire a toast |
| Feature flags | Server cache or Global | Server cache if fetched at runtime; global if baked at build |
Running through this exercise on every new feature catches 80 % of “wrong tool” decisions.
🪤 Common Pitfalls
- Treating “shared between two components” as “needs global state” — sometimes it’s just lift-state-up.
- Putting server data in Zustand — you reinvent half of TanStack Query, badly.
- Putting form input state in URL — every keystroke is a navigation; URL gets noisy.
- Using
useContextfor high-frequency-changing values — re-render storm. - URL state without a schema — bookmarks break when you rename a param.
- Local state for a value the user expects to survive reload — accidental data loss.
✅ Recap
- Four kinds: local, global, server cache, URL.
- The kind dictates the tool, not the other way around.
- Most “I need Redux” instincts are really “I need TanStack Query” or “I need URL state.”
- Context is the right tool for stable, low-frequency values — not as a general-purpose store.
- Audit one screen at a time; correcting the kind usually deletes code.
🔗 Further Reading
- Kent C. Dodds — “Application State Management with React” — the canonical decision-tree post.
- TKDodo — “Why I don’t like reduce” series and the TanStack Query thinking.
- Ch 13 (Global), Ch 14 (Server cache), Ch 15 (URL).
In the book — not on the site
Each topic has an 🧠 Under-the-hood subsection — the algorithm, the data structures, what React DevTools surfaces, debugging recipes. Plus a 🧪 hands-on lab per chapter with a starter repo. Reserved for the book.