"Should I use useState, useReducer, or a store like Zustand?" usually gets answered with vibes. So I built all three running the same feature, side by side, with live render counts — because the thing that actually drives the decision is invisible until you watch it.
▶ Live demo: https://state-mgmt-compare.vercel.app/
Source (React 19 + real Zustand): https://github.com/dev48v/state-mgmt-compare
Two views read two slices — count and name. Click "bump count" in each column and watch the per-view render counters.
The part everyone gets backwards
useState vs useReducer is not a performance decision. They re-render identically. It's an organisation decision — a pile of setX calls vs one reducer that handles related transitions in one place. Use a reducer when several values change together or the transitions get gnarly; use useState otherwise. That's it.
The decision that actually affects re-renders is Context vs an external store:
// Context: ANY change to the value re-renders EVERY consumer
const { count } = useContext(AppCtx); // re-renders when name changes too
// Zustand: subscribe to a SLICE — re-render only when THAT slice changes
const count = useStore((s) => s.count); // ignores name changes
In the demo, bumping count:
-
useState + Context → count view
×3, name view×3(both re-render) -
useReducer + Context →
×3/×3(identical — told you) -
Zustand → count view
×3, name view×1(never re-rendered)
That ×1 is the whole point. Zustand's selector subscribes the component to one slice of the store, so unrelated updates skip it — with no provider and no prop-drilling. To get the same result with Context you'd reach for React.memo, useMemo on the value, or splitting into multiple contexts.
So which one?
| useState | useReducer | Zustand | |
|---|---|---|---|
| Best for | a few local values | complex/related transitions | shared app state |
| Share across tree | lift + drill / Context | lift + drill / Context | import & subscribe |
| Re-render scope | Context: all consumers | Context: all consumers | only matching selector |
| Extra bundle | 0 | 0 | ~1 KB |
Use the simplest thing that fits. Reach for Zustand when state is shared widely and Context re-renders start to hurt — not before.
Real React, real Zustand, live counters. If it settled the question for you, a star helps others find it: https://github.com/dev48v/state-mgmt-compare
Top comments (0)