Choose based on scope (who needs the state) and update frequency:
- useState / useReducer — component-local state. First choice always.
- Lifting state up — share between sibling components via common ancestor.
- Context + useReducer — low-frequency global state (theme, auth, locale). Don't put rapidly-changing state here.
- Zustand / Jotai / Recoil — global state with fine-grained subscriptions. Components only re-render when their slice changes.
- Redux Toolkit — large teams, complex state, need time-travel debugging, strict patterns.
- React Query / SWR — server state (fetched data). Cache, invalidation, background refresh. NOT the same as UI state.
// Server state — always React Query or SWR, not useState
const { data, loading, error } = useQuery(['user', id], () => fetchUser(id));
// Global UI state (e.g. sidebar open) — Zustand
const useSidebar = create(set => ({
open: false,
toggle: () => set(s => ({ open: !s.open })),
}));
// Form state — react-hook-form or local useState
💡 80% of "state management" problems are actually server state problems. React Query eliminates huge amounts of boilerplate (loading, error, refetch, caching) that people used to put in Redux.