Managing State with useReducer
Managing State with useReducer
An attendance screen has several buttons that change a class report. Scattered updates make it difficult to see what each teacher action is allowed to change.
A reducer is a pure function that receives the current state and an action describing what happened, then returns the next state. useReducer gives a component that state and a dispatch function for sending actions.
Understand
Describe a teacher interaction as an action.
Build
Calculate state transitions in a pure reducer.
Check
Keep side effects out of reducer calculations.
An action becomes the next report
Event
The teacher marks one student present.
Reducer
Current count and action produce the next count.
Render
React describes the updated attendance report.
Separate the request from the update rule
With useState, a handler often expresses the update directly. A reducer moves that calculation into one named function. A handler can dispatch an action such as studentMarked, while the reducer decides how the attendance report changes. This is useful when many events affect the same related information.
Pure means that the same state and action produce the same answer without changing outside things. The reducer should not send a request, write browser storage or mutate its input. Its output is a new state value. The action is ordinary JavaScript data, and its type names are choices made by this application.
Describe attendance changes as actions
The state has present and total. The reducer allows marking another student only while present is below total, and reset returns to zero. The button handlers send requests. This keeps the upper bound in one place rather than relying only on whether a button happens to be disabled.
React · JavaScript with JSX
import { useReducer } from 'react';
function attendanceReducer(state, action) {
switch (action.type) {
case 'studentMarked':
return { ...state, present: Math.min(state.total, state.present + 1) };
case 'reset':
return { ...state, present: 0 };
default:
throw new Error('Unknown attendance action');
}
}
export default function App() {
const [state, dispatch] = useReducer(attendanceReducer, { present: 0, total: 3 });
return <main><h1>Attendance reducer</h1><p>Present: {state.present} of {state.total}</p>
<button onClick={() => dispatch({ type: 'studentMarked' })}>Mark one present</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset attendance</button>
</main>;
}Expected screen and behavior
Initially 0 of 3 are present. Mark one present increases the count up to 3. Reset attendance returns it to 0.Follow dispatch into the next render
React · Describe attendance changes as actions
import { useReducer } from 'react';
function attendanceReducer(state, action) {
switch (action.type) {
case 'studentMarked':
return { ...state, present: Math.min(state.total, state.present + 1) };
case 'reset':
return { ...state, present: 0 };
default:
throw new Error('Unknown attendance action');
}
}
export default function App() {
const [state, dispatch] = useReducer(attendanceReducer, { present: 0, total: 3 });
return <main><h1>Attendance reducer</h1><p>Present: {state.present} of {state.total}</p>
<button onClick={() => dispatch({ type: 'studentMarked' })}>Mark one present</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset attendance</button>
</main>;
}Clicking Mark one present dispatches studentMarked. React supplies the current report and that action to attendanceReducer. Starting from 0, Math.min chooses 1, and spreading state preserves total. React remembers the returned object and the next rendered paragraph says Present: 1 of 3.
The old handler still belongs to its render snapshot; dispatch does not immediately rewrite its state variable. Further actions use the queued state transitions. React may perform extra development checks under StrictMode to detect impure reducer behavior, so a reducer must remain correct when called again. The lesson does not depend on a particular call count.
Choose reducers for readable transitions
useReducer is not inherently more powerful than useState. A simple text field can stay with useState. Prefer a reducer when several related rules are easier to read together, or when an action has to update multiple related fields without allowing contradictory combinations.
Actions should describe events with the data needed to handle them. For example, a studentMarked action might include studentId in a real register. Do not put a browser click event into a reducer merely to read its target later; extract the meaningful student data in the event handler.
Try the school task
React · starting example
import { useReducer } from 'react';
function attendanceReducer(state, action) {
switch (action.type) {
case 'studentMarked':
return { ...state, present: Math.min(state.total, state.present + 1) };
case 'reset':
return { ...state, present: 0 };
default:
throw new Error('Unknown attendance action');
}
}
export default function App() {
const [state, dispatch] = useReducer(attendanceReducer, { present: 0, total: 3 });
return <main><h1>Attendance reducer</h1><p>Present: {state.present} of {state.total}</p>
<button onClick={() => dispatch({ type: 'studentMarked' })}>Mark one present</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset attendance</button>
</main>;
}Add an undoLastMark action that decreases present but never below zero. Start at zero, mark twice, and undo once: the count should be one. Keep the class size intact and make the lower-bound rule part of the reducer itself.
Worked answer
The answer returns a new object in both cases and uses Math.max for the lower bound. Try Undo last mark immediately after loading: the visible count remains zero. Then mark twice and undo. Those checks exercise both the boundary and an ordinary transition.
React · JavaScript with JSX
import { useReducer } from 'react';
function reducer(state, action) {
if (action.type === 'mark') return { ...state, present: Math.min(state.total, state.present + 1) };
if (action.type === 'undoLastMark') return { ...state, present: Math.max(0, state.present - 1) };
throw new Error('Unknown action');
}
export default function App() {
const [state, dispatch] = useReducer(reducer, { present: 0, total: 3 });
return <main><h1>Undo attendance</h1><p>Present: {state.present} of {state.total}</p>
<button onClick={() => dispatch({ type: 'mark' })}>Mark present</button>
<button onClick={() => dispatch({ type: 'undoLastMark' })}>Undo last mark</button>
</main>;
}Expected screen and behavior
Undo at zero stays at zero. Two marks followed by undo leave Present: 1 of 3.Mistakes and boundaries
Mutating state.present and returning the same object breaks the read-only state model. Reducers must return the whole next state; returning only present would lose total. Throwing for unknown actions exposes a programming error instead of silently hiding it.
A reducer organizes local transitions. It does not automatically share state across pages, save attendance, provide undo history or verify a teacher’s identity. This undo example only reverses a count; a real per-student undo needs enough information to identify which record changed.
Check your reasoning
React · Describe attendance changes as actions
import { useReducer } from 'react';
function attendanceReducer(state, action) {
switch (action.type) {
case 'studentMarked':
return { ...state, present: Math.min(state.total, state.present + 1) };
case 'reset':
return { ...state, present: 0 };
default:
throw new Error('Unknown attendance action');
}
}
export default function App() {
const [state, dispatch] = useReducer(attendanceReducer, { present: 0, total: 3 });
return <main><h1>Attendance reducer</h1><p>Present: {state.present} of {state.total}</p>
<button onClick={() => dispatch({ type: 'studentMarked' })}>Mark one present</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset attendance</button>
</main>;
}Choose the answer that follows the data and the interaction, then read the explanation for each alternative.
Carry the idea forward
Dispatch describes an event, the reducer calculates the next state, and React renders that result. Read controlled inputs next to connect remembered state to a teacher’s text entry.
Read the primary reference: React: useReducer reference.
Reaching the recap and completing the quizzes are separate milestones.