useReducer
Reviewed & published by Brayan K
useReducer is an alternative to useState for managing more complex state. You describe how state changes in one pure reducer function — (state, action) => newState — and components trigger changes by dispatching actions. All your update logic lives in one place, which makes complex state predictable and easy to test.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- What a reducer is: (state, action) => newState
- How dispatch sends actions to the reducer
- The shape of an action: { type, payload }
- Why reducers must be pure and immutable
- Spreading state to return a new object
- When to choose useReducer over useState
1️⃣ A Reducer Is (state, action) => newState
The reducer is a plain function that takes the current state and an action (an object describing what happened, usually with a type) and returns the next state. A switch on action.type keeps it tidy.
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
</>
);
}// A reducer is a pure function: (state, action) => newState.
function reducer(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; // unknown action: no change
}
}
let state = { count: 0 };
state = reducer(state, { type: "increment" }); // { count: 1 }
state = reducer(state, { type: "increment" }); // { count: 2 }
state = reducer(state, { type: "decrement" }); // { count: 1 }
console.log(state.count); // 12️⃣ dispatch — Sending Actions
useReducer gives you [state, dispatch]. You don't change state directly; you call dispatch(action), React runs the reducer with the current state, stores the result, and re-renders. Actions often carry data: { type: "add", amount: 10 }.
dispatch({ type: "add", amount: 10 }); // reducer adds 10
dispatch({ type: "clear" }); // reducer resets to 0// dispatch(action) runs the reducer and stores the result.
// We simulate React's dispatch with a tiny store.
function reducer(state, action) {
switch (action.type) {
case "add": return { total: state.total + action.amount };
case "clear": return { total: 0 };
default: return state;
}
}
let state = { total: 0 };
function dispatch(action) { state = reducer(state, action); }
dispatch({ type: "add", amount: 10 }); // total -> 10
dispatch({ type: "add", amount: 5 }); // total -> 15
dispatch({ type: "clear" }); // total -> 0
dispatch({ type: "add", amount: 3 }); // total -> 3
console.log(state.total); // 3Your turn — complete a toggle reducer for a light switch:
// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// Complete a toggle reducer for a light switch.
function reducer(state, action) {
switch (action.type) {
// 1) "toggle" should flip state.on (true <-> false), keeping other fields.
case "toggle": return { ...state, on: ___ }; // 👉 !state.on
// 2) "off" should set on to false.
case "off": return { ...state, on: ___ }; // 👉 false
default: return state;
}
}
let state = { on: false };
state = reducer(state, { type: "toggle" }); // on -> true
state = reducer(state, { type: "toggle" }); // on -> false
state = reducer(state, { type: "toggle" }); // on -> true
console.log(state.on); // true
state = reducer(state, { type: "off" });
console.log(state.on); // false
// ✅ Expected output:
// true
// false3️⃣ Reducers Must Be Pure & Immutable
A reducer must not mutate the old state or cause side effects — given the same inputs it always returns the same output. To change one field, spread the old state into a new object and override the field: { ...state, name: action.name }. This produces a fresh reference, which is how React detects the change.
// ❌ mutates the old state — React may miss the change
case "rename": state.name = action.name; return state;
// ✅ returns a new object — React sees a new reference
case "rename": return { ...state, name: action.name };// IMMUTABILITY: return a NEW object; never mutate the old state.
function reducer(state, action) {
switch (action.type) {
case "setName": return { ...state, name: action.name }; // copy + change one field
case "birthday": return { ...state, age: state.age + 1 };
default: return state;
}
}
const start = { name: "Ada", age: 36 };
const next = reducer(start, { type: "birthday" });
console.log(start.age); // 36 <- original untouched
console.log(next.age); // 37 <- new object with the change
console.log(start === next); // false <- new reference, so React re-renders🧩 Reorder Challenge
These lines form a complete counter reducer. Put them in the right order:
A. case "inc": return { count: state.count + 1 };
B. function reducer(state, action) {
C. default: return state;
D. switch (action.type) {
E. }
F. }Common Errors (and the fix)
- Nothing updates on dispatch. — Your reducer mutated state instead of returning a new object. Spread: { ...state, ... }.
- State becomes undefined. — A case forgot to return, or there's no default returning state.
- Typo in action type. — "incremnt" falls through to default and silently does nothing. Watch your strings.
- Doing side effects in the reducer. — Reducers must be pure. Do fetches/logging in event handlers or effects, not inside the reducer.
📋 Quick Reference
| Piece | Code |
|---|---|
| Hook | const [state, dispatch] = useReducer(reducer, init); |
| Reducer | function reducer(state, action) { ... } |
| Action | { type: "add", amount: 10 } |
| Dispatch | dispatch({ type: "add", amount: 10 }) |
| Update a field | return { ...state, field: value }; |
🧠 Quick Recall
1. What is the signature of a reducer?
(state, action) => newState — a pure function returning the next state.
2. A reducer does state.count++; return state;. What's wrong?
It mutates the old state and returns the same reference, so React may not re-render. Return { ...state, count: state.count + 1 }.
3. You dispatch { type: "nope" } and the reducer has a default: return state. What happens?
Nothing changes — the default returns the existing state unchanged.
Mini-Challenge: A Cart Reducer
Write a reducer handling addItem, removeItem, and empty, then dispatch a sequence and log the final count. Run it and check your output.
// 🎯 MINI-CHALLENGE: a cart reducer
//
// Write reducer(state, action) handling:
// "addItem" -> { ...state, count: state.count + 1 }
// "removeItem" -> { ...state, count: state.count - 1 }
// "empty" -> { ...state, count: 0 }
// default -> state
//
// Then dispatch these actions in order and log the final count:
// addItem, addItem, addItem, removeItem
//
// ✅ Expected output:
// 2
let state = { count: 0 };
// your code here🎉 Lesson Complete
- ✅ A reducer is a pure (state, action) => newState
- ✅ dispatch(action) runs the reducer and re-renders
- ✅ Actions are objects, usually { type, payload }
- ✅ Stay immutable — spread state, never mutate; always have a default
Practice quiz
What is the signature of a reducer?
- (action) => state
- (state) => action
- (state, action) => newState
- (newState, oldState) => action
Answer: (state, action) => newState. A reducer is a pure function (state, action) => newState that computes the next state from the current state and an action.
What does dispatch(action) do?
- Runs the reducer with the current state and action, stores the result, and re-renders
- Mutates state directly
- Creates a new reducer
- Returns the previous state
Answer: Runs the reducer with the current state and action, stores the result, and re-renders. dispatch sends an action to the reducer; React runs reducer(currentState, action), stores the returned value as new state, and re-renders.
What does useReducer return?
- [state, setState]
- [dispatch, reducer]
- [reducer, action]
- [state, dispatch]
Answer: [state, dispatch]. useReducer(reducer, initialState) returns a [state, dispatch] pair. You call dispatch(action) to trigger updates.
Why must a reducer be pure and avoid mutating the old state?
- To make it run faster
- React compares state by reference; mutating keeps the same reference and updates can be missed
- Because mutation is a syntax error
- So the reducer can be async
Answer: React compares state by reference; mutating keeps the same reference and updates can be missed. React compares previous and next state by reference. Mutating the old object keeps the same reference, so React may not detect the change. Return a new object.
How do you correctly update one field of state in a reducer?
- return { ...state, name: action.name };
- state.name = action.name; return state;
- return action.name;
- state = { name: action.name };
Answer: return { ...state, name: action.name };. Spread the old state into a new object and override the field: { ...state, name: action.name }. This produces a fresh reference.
What is the typical shape of an action?
- A string only
- A function
- An object, usually { type, payload }
- An array of states
Answer: An object, usually { type, payload }. Actions are usually objects with a type describing what happened, often plus a payload of data, e.g. { type: 'add', amount: 10 }.
You dispatch { type: 'nope' } and the reducer has default: return state. What happens?
- React throws an error
- Nothing changes — the default returns the existing state
- State becomes undefined
- The reducer runs twice
Answer: Nothing changes — the default returns the existing state. An unknown action type falls through to the default case, which returns the existing state unchanged, so nothing updates.
When is useReducer a better choice than useState?
- When you have a single boolean
- When you never update state
- When you want to avoid a render
- When state is complex with several related fields, or many handlers update it in different ways
Answer: When state is complex with several related fields, or many handlers update it in different ways. useReducer centralizes complex update logic in one place — ideal for complex state, or when many handlers update the same state differently.
A reducer does state.count++; return state;. What is wrong?
- Nothing, it is correct
- It mutates the old state and returns the same reference, so React may not re-render
- count cannot be incremented
- It needs a payload
Answer: It mutates the old state and returns the same reference, so React may not re-render. It mutates state and returns the same reference. Return a new object: { ...state, count: state.count + 1 }.
Where should side effects like data fetching go?
- Inside the reducer
- In the default case
- In event handlers or effects, not in the reducer
- In the action object
Answer: In event handlers or effects, not in the reducer. Reducers must be pure. Do fetches, logging, and other side effects in event handlers or effects, never inside the reducer.
Continue this course
- Previous: useContext & the Context API
- Next: Custom Hooks — Extract and reuse stateful logic in your own hooks
- Quick reference: React cheat sheet
Frequently asked questions
What is a reducer?
A pure function (state, action) => newState. Given the current state and an action describing what happened, it computes and returns the next state. It must not mutate the old state — it returns a new one.
When should I use useReducer instead of useState?
When state is complex (several related fields), when the next state depends heavily on the previous, or when many event handlers update the same state in different ways. A reducer centralizes all that logic in one place.
What does dispatch do?
dispatch(action) sends an action object (usually { type, payload }) to the reducer. React runs reducer(currentState, action), stores the returned value as the new state, and re-renders.
Why must the reducer be pure and avoid mutation?
React compares the previous and next state by reference to decide what changed. If you mutate the old object, the reference is the same and updates can be missed. Always return a new object/array (spread the old one).