React state management is not a single tool — it’s a spectrum from local component state to global app state, and picking the wrong layer creates unnecessary complexity. useState for simple local state, useReducer for complex local state, Context for configuration, Zustand for app-wide state.
⚡ TL;DR: useState for simple primitive/object local state. useReducer when next state depends on previous in complex ways. Context for global config/theme/auth (not high-frequency updates). Zustand for global state with frequent updates — simpler than Redux, more capable than Context.
useState — simple local state
// Simple state: use useState
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Independent pieces of state — separate useState calls
return ;
}
// Object state: OK for related values
function Form() {
const [form, setForm] = useState({name:'',email:'',age:''});
const update = field => e =>
setForm(prev=>({...prev,[field]:e.target.value}));
return (
<>
>
);
}
useReducer — complex state transitions
// Use when: multiple sub-values, next state depends on previous
const initialState = { status:'idle', data:null, error:null };
function reducer(state, action) {
switch(action.type) {
case 'FETCH_START': return {...state, status:'loading', error:null};
case 'FETCH_SUCCESS': return {status:'success',data:action.payload,error:null};
case 'FETCH_ERROR': return {...state, status:'error', error:action.error};
case 'RESET': return initialState;
default: throw new Error('Unknown action: '+action.type);
}
}
function DataFetcher({url}) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({type:'FETCH_START'});
fetch(url).then(r=>r.json())
.then(data=>dispatch({type:'FETCH_SUCCESS',payload:data}))
.catch(err=>dispatch({type:'FETCH_ERROR',error:err.message}));
}, [url]);
}
Context — global config, not frequent state
// GOOD: Context for slow-changing global values
const ThemeContext = createContext('light');
const AuthContext = createContext(null);
// BAD: Context for frequent updates (every update re-renders all consumers)
// Don't use Context for: shopping cart (updated on every add/remove)
// Do use Context for: theme, locale, auth user, feature flags
function App() {
const [theme, setTheme] = useState('light');
return (
);
}
// Consume:
const {theme} = useContext(ThemeContext);
Zustand — simple global state
import {create} from 'zustand';
// Define store
const useCartStore = create((set,get) => ({
items: [],
total: 0,
addItem: (item) => set(state => ({
items: [...state.items, item],
total: state.total + item.price
})),
removeItem: (id) => set(state => ({
items: state.items.filter(i=>i.id!==id),
total: state.total - (state.items.find(i=>i.id===id)?.price||0)
})),
clear: () => set({items:[],total:0})
}));
// Use in any component (no Provider needed!)
function CartIcon() {
const {items} = useCartStore();
return {items.length};
}
- ✅ useState: primitive values, independent state pieces
- ✅ useReducer: multiple related values, complex transitions
- ✅ Context: slow-changing global (theme, auth, locale)
- ✅ Zustand: global state with frequent updates — simpler than Redux
- ❌ Don’t use Context for high-frequency updates — all consumers re-render
- ❌ Don’t install Redux for small apps — overkill, Zustand is simpler
External reference: Zustand documentation.
Recommended Reading
→ Designing Data-Intensive Applications — The bible of distributed systems and production engineering at scale.
→ The Pragmatic Programmer — Timeless engineering wisdom every senior developer needs.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Join 2,000+ Senior Developers
Get expert-level JavaScript, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
