State with useReducer
Replace tangled useState calls with a reducer when updates follow many related transitions.
- Recognise when several state values move together.
- Model each transition as an action with a type field.
- Write a pure reducer that returns new state without mutating.
- Dispatch actions from event handlers instead of calling many setters.
In plain English
When a component juggles many pieces of state that change together, useState calls start to tangle. A reducer is a pure function that takes the current state and an action, then returns the next state. An action is a plain object that describes what happened, usually with a type field. useReducer hands you the state and a dispatch function, so every transition is explicit and grouped in one place.
Step by step
- 1List every event the component can receive.
- 2Define one action type for each event, carrying only the needed data.
- 3Write the reducer as a pure switch over the action types.
- 4Replace the setters with dispatch calls in the handlers.
import { useReducer } from 'react'
type QuizState = {
index: number
answers: Record<number, number>
}
type QuizAction =
| { type: 'answer'; question: number; choice: number }
| { type: 'next' }
| { type: 'reset' }
function reducer(state: QuizState, action: QuizAction): QuizState {
switch (action.type) {
case 'answer':
return { ...state, answers: { ...state.answers, [action.question]: action.choice } }
case 'next':
return { ...state, index: state.index + 1 }
case 'reset':
return { index: 0, answers: {} }
}
}
function Quiz() {
const [state, dispatch] = useReducer(reducer, { index: 0, answers: {} })
function choose(choice: number) {
dispatch({ type: 'answer', question: state.index, choice })
}
return <p>Question {state.index + 1}</p>
}Common mistakes
- Reaching for useReducer for a single boolean is unnecessary and makes the component harder to read.
- Mutating the current state inside a reducer means React cannot see that anything changed.
- Putting side effects such as fetching inside the reducer breaks its purity and causes repeated work.
- Copying one logical update across many separate useState calls makes some transitions impossible to keep consistent.
Add a reset action
Extend a quiz reducer with a reset action that returns the initial state, and a button that dispatches it.
type QuizState = { index: number; score: number }
function reducer(state: QuizState, action: { type: 'correct' }): QuizState {
if (action.type === 'correct') {
return { ...state, score: state.score + 1 }
}
return state
}What must a reducer be?
Make it yours.
Keep a private note for this lesson, then turn the key ideas into flashcards you can revisit.
Lock in what you just learned.
Mark this lesson complete to save your place and move one step closer to the certificate.