State management in React ranges from simple useState to complex global stores. The key is using the right tool for each situation—most apps need less than you think.
State Categories#
Local State:
- Form inputs
- UI toggles
- Component-specific data
→ useState, useReducer
Shared State:
- Theme, locale
- User session
- Feature flags
→ Context, Zustand
Server State:
- API responses
- Cached data
- Loading states
→ TanStack Query, SWR
URL State:
- Current page
- Filters, search
- Shareable state
→ Router params, searchParams
useState for Local State#
useReducer for Complex State#
Context for Shared State#
Zustand for Global State#
TanStack Query for Server State#
Decision Guide#
Use useState when:
- State is local to component
- State is simple (primitives, small objects)
- No need to share with siblings
Use useReducer when:
- Complex state logic
- Multiple related values
- Next state depends on previous
Use Context when:
- Theme, locale, auth
- Infrequently updated state
- Avoid prop drilling (2-3 levels)
Use Zustand when:
- Global app state
- Frequent updates
- Need persistence
- Don't want providers
Use TanStack Query when:
- API data fetching
- Caching needed
- Background refetching
- Optimistic updates
Skip Redux unless:
- Large team needs conventions
- Complex middleware requirements
- Time-travel debugging critical
Best Practices#
DO:
✓ Start with useState
✓ Colocate state with UI
✓ Split state by domain
✓ Use server state libraries for API data
✓ Derive state when possible
DON'T:
✗ Put everything in global state
✗ Use Context for frequently updating state
✗ Duplicate server state in client state
✗ Reach for Redux by default
Conclusion#
Most React apps need less state management than you think. Use useState for local state, TanStack Query for server state, and Zustand for the rare global client state.
Start simple and add complexity only when needed.