DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

useState vs useReducer vs Zustand: I Built the Same Feature Three Ways (With Live Render Counts)

"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
Enter fullscreen mode Exit fullscreen mode

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)