Skip to content
yutils

How State Management Actually Works

Local vs global state, why Context API isn't a Redux replacement (re-render performance), Zustand vs Jotai vs Redux differences, server state vs client state (TanStack Query / SWR), and when 'no library' is the right answer.

~9 min read

First 50 KB decision of every React project — Redux? Zustand? Jotai? Context? "Just use useState" — then props drilling. Switch to Context? — then re-render storms. This guide covers each tool's trade-offs, the server-state vs client-state split, and when "no library" is the right answer.

Local vs Global — The First Question

Local state (useState):
  - Used by one component + some children
  - Simple — useState alone is enough
  - Examples: form input, modal open/close, hover state

Global state:
  - Shared across pages or distant components
  - Examples: logged-in user, theme, cart, notifications

→ 95% start local. Lift to a store only when global need is clear.

Context API — Not a Redux Replacement

Common usage:
  const UserContext = createContext(null);

  function App() {
    const [user, setUser] = useState(null);
    return (
      <UserContext.Provider value={{user, setUser}}>
        <Header />  ← every child can access user
        <Main />
      </UserContext.Provider>
    );
  }

Problem — when Context value changes:
  - Every descendant using useContext re-renders
  - The object identity is fresh per render (= new reference) → re-render guaranteed

  // ❌ value is a new object every render
  <UserContext.Provider value={{user, setUser}}>

  // ✓ Stabilize with useMemo
  const value = useMemo(() => ({user, setUser}), [user]);
  <UserContext.Provider value={value}>

  // Still: when user changes, every consumer re-renders
  → If only one component uses user.name, others still re-render

→ Context is for "avoiding prop drilling". Bad for frequently-changing state.
  Dedicated stores (Redux / Zustand / Jotai) do fine-grained subscription.

Redux — The Classic Heavyweight

// Single store + action + reducer
const store = createStore(rootReducer);
store.dispatch({type: "SET_USER", payload: user});

// React integration (react-redux)
const user = useSelector(state => state.user);

Pros:
- Strong pattern (action / reducer / selector)
- DevTools (time-travel debugging)
- Middleware (saga, thunk)
- Selectors trigger re-render only for changed slices

Cons:
- High boilerplate (action types, reducers, selectors)
  → Redux Toolkit (RTK) reduces this
- Steep learning curve
- "Everything in the store" anti-pattern temptation

Modern: Redux Toolkit (RTK) + RTK Query — keep Redux strengths, less boilerplate

Zustand — The Minimal Camp

import {create} from "zustand";

const useUserStore = create((set) => ({
  user: null,
  setUser: (user) => set({user}),
}));

// In a component:
const user = useUserStore(state => state.user);
const setUser = useUserStore(state => state.setUser);

Pros:
- Very simple (no action types, just call set)
- Small bundle (~1 KB)
- TypeScript-friendly
- Fine-grained subscription via selectors

Cons:
- Redux DevTools integration possible but not default
- Smaller middleware ecosystem than Redux

→ Recommended first choice for new projects (2024+ trend).

Jotai — Atomic State

import {atom, useAtom} from "jotai";

const countAtom = atom(0);
const doubledAtom = atom(get => get(countAtom) * 2);  // derived

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubled] = useAtom(doubledAtom);  // auto-recompute
  return <button onClick={() => setCount(count+1)}>{count} → {doubled}</button>;
}

Pros:
- "Atomic" model — graph of tiny atoms (spiritual successor to Recoil)
- Automatic dependency tracking (derived atoms)
- Tiny (~1 KB)

Cons:
- Mental model takes adjusting
- Atom management can complicate large apps

Use cases: forms or many small reactive units.

Server State vs Client State — The Biggest Insight

Client state: transient UI state
  - modal open, current tab, selected filter
  - reset-on-refresh is fine
  - useState / useReducer / Zustand

Server state: client-side cache of server data
  - user info, post lists, search results
  - server is source of truth
  - need cache / refetch / mutation invalidation

→ Handling both with the same tool (e.g. Redux) means manually
   managing server-state cache / loading / error (boilerplate explosion).

TanStack Query (formerly React Query) — server-state specialist:
  const {data, isLoading, error} = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then(r => r.json()),
  });
  // cache + loading + error + refetch + stale-while-revalidate, automatic

SWR (Vercel) — similar tool.

Modern trend:
  - server state: TanStack Query or SWR
  - client state: useState (local) + Zustand (global)
  - Clean separation → less boilerplate + better UX

When "No Library" Is the Right Answer

  • Most small apps — useState + props is enough. 2-3 levels of drilling are tolerable.
  • Isolated widgets — calculators, color pickers, anything self-contained. Local state only.
  • Simple forms — react-hook-form or useState (think twice before adding a form library).
  • With RSC — some server state arrives as server component props naturally (sometimes you don't even need TanStack Query).

Common Pitfalls

  • Fresh Context value per render — performance disaster. useMemo to stabilize.
  • Everything in Redux — even modal open in the store bloats it. Local stays useState.
  • Managing server state in your own store — cache / refetch boilerplate explodes. Lean on TanStack Query.
  • Premature optimization — full Redux setup on a small app is over-engineering.
  • Mixing store libraries — Zustand + Redux + Jotai simultaneously is cognitive overhead. Pick one or two.

Wrap-up

Big insight in state management = server state ≠ client state. Server state belongs to TanStack Query / SWR. Client state combines useState (local) + Zustand (global) in the modern stack.

Practical: new project default — useState + TanStack Query. Add Zustand when global client state is needed. Legacy / large enterprise — Redux Toolkit. Context is for static values (theme, locale).

Back to guides