useReducer manages state through a reducer function and dispatched actions. It is useful when many related state transitions need clear, centralized rules.
1 Basic Reducer
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
default:
return state;
}
}
2 Syntax
const [state, dispatch] = useReducer(reducer, initialState);
3 Reducer Flow
flowchart TD
A[User Action] --> B[dispatch]
B --> C[Reducer]
D[Current State] --> C
C --> E[Next State]
E --> F[Component Renders]
4 Counter Example
function counterReducer(state, action) {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "decrement": return { count: state.count - 1 };
case "reset": return { count: 0 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(
counterReducer,
{ count: 0 }
);
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
5 Actions and Payloads
dispatch({
type: "addUser",
payload: { id: 1, name: "Rahul" }
});
An action describes what happened. The payload carries data needed for the transition.
6 Reducers Should Be Pure
A reducer should calculate the next state. Do not perform API calls, timers, or other side effects inside it.
7 API State
const initialState = {
users: [],
loading: false,
error: null
};
function userReducer(state, action) {
switch (action.type) {
case "loadStart":
return { ...state, loading: true, error: null };
case "loadSuccess":
return { users: action.payload, loading: false, error: null };
case "loadFailure":
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
8 API With useReducer
useEffect(() => {
async function loadUsers() {
dispatch({ type: "loadStart" });
try {
const response = await fetch("/api/users");
if (!response.ok) throw new Error("Request failed");
dispatch({
type: "loadSuccess",
payload: await response.json()
});
} catch (error) {
dispatch({ type: "loadFailure", payload: error.message });
}
}
loadUsers();
}, []);
9 useState vs useReducer
| useState | useReducer |
|---|---|
| Simple state is easy | Complex transitions can be centralized |
| Setter functions | Dispatch actions |
| Good for independent values | Good for related transitions |
10 When to Use It
- Many actions modify related state.
- Several values change together.
- Transition rules are difficult to follow.
- You want state logic in one place.
11 Common Mistakes
- Mutating state in a reducer.
- Calling APIs inside a reducer.
- Forgetting a default action case.
- Using useReducer when useState is clearer.
12 Interview Questions
- What is useReducer?
- What is a reducer?
- What is an action?
- What does dispatch do?
- Why should reducers be pure?
- When should useReducer be preferred over useState?
13 Key Takeaways
- useReducer uses actions and a reducer to manage state.
- dispatch sends actions.
- The reducer returns the next state.
- Reducers should be pure.
Conclusion
useReducer provides an explicit state-transition model. It is valuable when related state becomes complex and centralized transition rules improve clarity.