React state: when I use what
Most React state bugs I review aren't state bugs. They're one mistake wearing four different costumes: treating server data like local data.
Here's the ladder I actually climb, in order.
1. useState — until it hurts.
One component owns it, nobody else needs it. That covers more cases than people expect. I don't reach for a library because an app "feels big enough" — I reach when passing props gets genuinely painful, and not one step before.
2. Context — for things that rarely change.
Theme, locale, the current user. Context is a delivery mechanism, not a state manager: every consumer re-renders when the value changes. Put a fast-changing value in there and you've built a performance problem that's hard to see and harder to trace.
3. Zustand — for client state that's actually shared.
UI state several unrelated parts of the app read and write: a wizard's progress, filters, a drawer. Small, no boilerplate, no provider wrapping. I use it at Navoy and I've never wanted the ceremony of Redux back for this kind of thing.
4. TanStack Query — for anything that came from the server.
This is the one that changes how the whole app feels. Server data isn't state you own, it's a cache of someone else's state — it goes stale, it needs refetching, it can fail, two components can ask for it at once. Once that moved out of my "state management", most of my global state simply disappeared. A lot of Redux stores I've seen were just a hand-rolled, buggier version of this.
The honest nuance: none of this is a rule. A three-screen app with useState everywhere and no libraries is a perfectly good app. The cost of the wrong abstraction is higher than the cost of a bit of prop drilling.
The question was never "which state library". It's "who owns this data" — and the answer is usually not you.
Where do you draw the line between useState and reaching for a library?