State Management Architecture

Stop asking “which state library?” and start asking “what KIND of state is this?” Split state into server · URL · local · shared · form, pick the right tool per kind, and ~90% of your “state management” problem disappears — no giant store required.

1The reframe — it's a taxonomy, not a library choice

The junior question is “Redux or Zustand?” The Lead question is “what kind of state am I holding, and what's the right tool for that?” Almost all app state falls into five concerns, each with a different owner and lifecycle: [developerway]

Kind of stateExampleRight tool
Server (remote/async)hotel list, prices, user bookingsTanStack Query / SWR
URL (query params)search filters, dates, tab, page #nuqs / the router
Local (one component)dropdown open, tooltip, input valueuseState / useReducer
Shared (UI, cross-component)theme, current user, sidebarprops → Context → store
Formcheckout fields, validationRHF / form lib (a specialised mix)

The punchline: most of what teams shove into a global store is actually server state or URL state in disguise. Pull those out into purpose-built tools and the “global state” you have left is small, predictable, and easy to reason about. “Break your state into different concerns and you'll find yourself with better solutions.”

One-liner

“I don't start with a library — I start by naming the state. Server, URL, local, shared, form. Pick the right tool per kind and 90% of the ‘state management problem’ just evaporates.”

2The big one — server state ≠ client state

This is the distinction the panel is fishing for. Client state you own: it's synchronous, lives in the browser, and you're the source of truth (is the modal open?). Server state you don't own — it's a cache of someone else's data: [Kent C. Dodds]

Client / UI state

you are the source of truth

Synchronous, instantly correct, lives only in the tab. Modal open, selected tab, form draft. useState / Context / Zustand.

Server state

a cache — the server owns the truth

Async, goes stale, shared by many components, needs caching, dedupe, retries, invalidation, pagination, optimistic updates. useState can't model any of that.

Putting server data in Redux/Zustand means hand-reinventing caching, request dedupe, background refetch, and invalidation — badly. That's why ~80% of the Redux in legacy apps is just server-state plumbing. A server-state library (TanStack Query / SWR) gives you all of it for free: stale-while-revalidate, dedupe, invalidate-on-mutation (Lesson 08). [TanStack]

// Server state — NOT useState/Redux. The library owns the cache.
const { data, isLoading } = useQuery({
  queryKey: ['hotel', id],          // cache key
  queryFn: () => fetchHotel(id),    // dedupe + SWR + retry, free
});
One-liner

“Server state isn't app state — it's a cache of data the backend owns. Modelling it with useState or Redux means re-implementing caching and invalidation by hand. React Query exists so you don't.”

3The underused one — URL state

The category most engineers forget — and the one that matters most for a travel/booking site. Anything that should survive a refresh, be shareable, bookmarkable, work with the back button, and be crawlable by Google belongs in the URL, not React state: search terms, dates, filters, sort, pagination, the active tab. [developerway]

For the platform this is huge: /search?city=tokyo&checkin=…&guests=2 must be a real, linkable, SEO-indexable URL (Lesson 09). The catch is manually syncing state ↔ URL is “a journey full of misery and weird bugs.” nuqs makes a query param behave like useState: [nuqs]

// The URL IS the state — shareable, refresh-proof, crawlable.
const [city, setCity] = useQueryState('city');   // nuqs — syncs to ?city=
Trade-off — not everything belongs in the URL. The URL is public, length-limited, and user-editable, so don't put secrets, huge blobs, or rapidly-changing values (a live scroll position) there. The rule: state that defines “what page am I looking at” and should be shareable → URL; ephemeral UI → local. Over-stuffing the URL creates history spam and re-render churn.
One-liner

“Filters, dates, sort, page — that's URL state, not React state. On a travel site it has to be shareable and crawlable, so the URL is the source of truth. I use nuqs so a query param behaves like useState.”

4Local & shared — and the Context trap

What's left after server + URL is genuine client state, and you escalate only as far as you need: [react.dev]

  1. LocaluseState / useReducer. Most UI state never needs to leave its component. “Zero reason to introduce a library” for a dropdown's open flag.
  2. Shared, shallowprop drilling is fine for 2–3 levels. Lift state to the nearest common parent (colocation).
  3. Shared, deepContext to skip the drilling — but mind the perf trap below.
Trade-off — Context's re-render trap (“providers hell”). Context is not a state manager — it's a transport. When a Context value changes, every consumer re-renders — all of them — whether or not they use the part that changed. Stuff sidebar + theme + user into one provider and any change re-renders the whole app. The usual fix is splitting into many tiny providers — at which point, per the article, “you're basically re-inventing Zustand, so you might as well use it.” Context is great for low-frequency, rarely-changing values (theme, current user); reach for a store when the value changes often or is read selectively.
One-liner

“Context is a transport, not a store: any value change re-renders every consumer. Perfect for theme or current user; the moment it changes often or I'm splitting providers to dodge re-renders, that's the signal to use a real store.”

5The client-store landscape — Zustand vs Jotai vs Redux

Only now — for client state that's shared, changes often, and is read selectively — does a store earn its place. The three you must compare: [Zustand]

Zustand

default pick — simple, one store

Create state, use it via a hook. No provider, selector-based granular re-renders, tiny API, SSR/RSC-friendly. The “versatile middle ground” for most apps.

Jotai

atomic, bottom-up

State is atoms; components subscribe to just the atoms they read, so updates are fine-grained by construction. Shines with many small, interdependent pieces of state.

Redux Toolkit

large, structured, audited

Opinionated, strict unidirectional flow, time-travel DevTools, middleware. Worth it for big apps/teams needing structure & debuggability — but heavier boilerplate.

Jotai vs Zustand

atoms vs one store

Jotai = bottom-up (compose atoms); Zustand = top-down (one store + selectors). Both avoid Context's re-render-everything problem.

“No provider” — what that actually means

A provider exists to inject a value into the React tree so descendants can read it via Context. Zustand never enters the tree: create() builds the store as a plain module-level closure — a normal JS object living in the module's scope. Any component that imports the hook subscribes directly to that external object (Zustand uses React's useSyncExternalStore under the hood). There's nothing to “provide” because the store isn't scoped to a subtree — it's a singleton the module already holds.

// store.js — the store IS a module-scoped object. No <Provider> anywhere.
import { create } from 'zustand';

export const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((s) => ({ items: [...s.items, item] })),
  clear:   () => set({ items: [] }),
}));
// CartBadge.jsx — import the hook anywhere; nothing wraps these components.
import { useCartStore } from './store';

function CartBadge() {
  // selector → re-renders ONLY when items.length changes
  const count = useCartStore((s) => s.items.length);
  return <span>🛒 {count}</span>;
}

// AddButton.jsx — somewhere else entirely in the tree, no shared ancestor needed
function AddButton({ product }) {
  const addItem = useCartStore((s) => s.addItem); // stable, never re-renders
  return <button onClick={() => addItem(product)}>Add</button>;
}

Contrast with Context, where you must wrap — <CartContext.Provider value={…}>…</CartContext.Provider> — and only descendants can read. Here CartBadge and AddButton share no provider ancestor; they just both import useCartStore.

Trade-off — “no provider” = singleton. Module scope means one store per app. Great in the browser; dangerous on the server (SSR/RSC), where a shared module would bleed one user's state into another request. For SSR you instead create the store per request and pass it down — the one case where Zustand does offer an optional provider. No provider also means no tree-scoped reset: you can't mount two isolated copies by nesting two providers the way Context allows.

The criteria that pick a winner (Makarevich's): simplicity, zero/single provider (no providers hell), granular re-rendering (selectors/atoms, not whole-tree), and React-aligned (hooks, SSR, RSC, unidirectional). On those, Zustand is the common default — “not because it's objectively best, but because it satisfies the criteria.” The modern default stack: TanStack Query + nuqs + Zustand.

One-liner

“For the client state that's left, I default to Zustand — no provider, selector re-renders, tiny API. Jotai if the state is atomic and interdependent; Redux Toolkit when a big team needs structure and DevTools. The days of putting everything in Redux are over.”

6The Platform angle & Lead framing

Apply the taxonomy to a hotel search/booking flow and watch the “global store” shrink to almost nothing:

StateKind → tool
Search results, hotel details, prices, bookingsServer → TanStack Query (SWR cache, dedupe, invalidate-on-book; Lesson 08)
City, dates, guests, filters, sort, pageURL → nuqs (shareable + crawlable for SEO; Lesson 09)
“Map/list” toggle, modal open, hovered cardLocaluseState
Theme, locale, auth userShared, low-freq → Context
Checkout form + validationForm → React Hook Form
Full loop

Concept: state management is a taxonomy — server/URL/local/shared/form — each with a purpose-built tool; the leftover global store is tiny. Trade-off: a single global store is simple to picture but reinvents caching for server data and re-renders everything via Context; specialised tools cost more concepts but remove whole bug classes. Anchor: “We deleted most of a Redux store by moving server data to React Query and search filters to URL state — fewer re-renders, shareable search links that helped SEO, and a fraction of the code.” Impact: at platform scale this is correctness and conversion — crawlable search URLs, instant-feeling cached data, no over-render jank on low-end devices. Invite: “If the team strongly valued strict structure and audit trails over minimalism, I'd run Redux Toolkit for the client slice instead of Zustand — same taxonomy, different store.”

7Check yourself — scenario quiz

Pick an answer; instant feedback. Push-back style, like the round.

1. A panelist asks “Redux or Zustand?” What's the strongest opening move?

2. The core distinction between server state and client state:

3. Why is putting fetched API data in Redux/Zustand usually the wrong call?

4. On a travel site, where do search filters, dates, sort, and page number belong?

5. What makes URL state especially important for the platform specifically?

6. What's the Context “providers hell” / re-render trap?

7. Given the re-render trap, what is Context genuinely good for?

8. Why do Zustand and Jotai avoid Context's re-render-everything problem?

9. The clean one-line difference between Zustand and Jotai:

10. When is Redux Toolkit still the right choice over Zustand?

11. A teammate wants one big global store for the whole hotel app. Best Lead pushback?

12. The modern “default stack” the taxonomy points to:

13. Zustand needs no provider. Why — and what's the one exception?

0 / 13 answered

Try this aloud before next session: “Architect state for a hotel search & booking flow. For each piece — results, filters, modal, theme, checkout form — name the kind of state and the tool, and explain why server state doesn't go in Redux and filters go in the URL.” Time to 90 seconds.
Good follow-up topics:
“Quiz me out loud, harder” “RTK Query vs React Query?” “Where does server state live with RSC?” “Context selector pattern / use-context-selector?” “Optimistic updates in React Query?” “Signals — do they change this?”