useReducer() VS useState()
The main difference
| Hook | What it's for |
|---|---|
useState | Simple, independent state (one value, short logic) |
useReducer | Complex update logic, many interrelated pieces of data, a clear structure of "actions" |
When to use useReducer()
1. When the update logic is complex or multi-step
If you have several types of changes that depend on different conditions,
instead of a bunch of setState and if, it makes more sense to have one centralized reducer.
Example:
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + state.step };
case "decrement":
return { ...state, count: state.count - state.step };
case "setStep":
return { ...state, step: action.payload };
default:
return state;
}
}Here useReducer is more convenient than three separate useState calls.
2. When the pieces of state are interrelated
If changing one field affects others, useReducer lets you
manage them in one place, instead of dozens of setState calls.
Example:
function formReducer(state, action) {
switch (action.type) {
case "CHANGE_FIELD":
return { ...state, [action.field]: action.value };
case "RESET":
return initialForm;
default:
return state;
}
}Managing a form with a single reducer is simpler and more reliable than 5 separate useState calls.
3. When you need predictable behavior and a "Redux approach"
If you want updates to be transparent, testable, and centralized
(similar to Redux), useReducer provides this with React's built-in tools.
"A reducer makes state deterministic: the same action always produces the same new state."
4. When updates depend on the previous state
Instead of writing a pile of setState(prev => ...),
the reducer handles this for you - all the logic is already inside reducer().
5. When you want to use context for global state
useReducer is very often combined with useContext to build
global state without Redux / Zustand:
const StoreContext = createContext();
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}This is a mini-Redux right inside React - clean, predictable, and without external dependencies.
When you do NOT need useReducer
| Case | Better to use |
|---|---|
| Simple state (boolean, number, string) | useState |
| Independent pieces of state | Several useState |
| The logic is short and obvious | useState is simpler and more readable |
| There are no different "action types" | useReducer is overkill |
Comparison example
With useState
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const increment = () => setCount(prev => prev + step);Simple and readable - perfect when the logic is simple.
With useReducer
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + state.step };
case "setStep":
return { ...state, step: action.payload };
default:
return state;
}
}More convenient if the logic grows - for example, if decrement, reset, double, and so on are added later.
Summary
| Criterion | useState | useReducer |
|---|---|---|
| Simple logic | yes | no |
| Complex interrelations | no | yes |
| Many types of change | no | yes |
| Centralized logic | no | yes |
| Ease of writing | yes | a bit more code |
| Global state (via context) | possible | ideal |
Main rule:
Use
useStatefor simple state, anduseReducerwhen state-management logic becomes "mini business logic".
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.