Introduction
useReducer is a React hook for managing more complex component state, especially when the next state depends on the previous one or when multiple sub-values change together in predictable ways. It follows the same pattern popularized by Redux: state transitions are described as pure functions that take the current state and an 'action' object, and return a new state. This makes state updates more predictable and testable than scattering multiple useState calls, particularly when several pieces of state are updated in response to the same event.
Cricket analogy: Instead of separately tracking runs, wickets, and overs with scattered notes, a scorer uses one official scorebook where each ball is an 'action' (four, wicket, wide) fed into a single scoring function that produces the next complete scoreline, keeping everything consistent.
Syntax
import { useReducer } from 'react';
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: action.payload ?? 0 };
default:
throw new Error(`Unknown action type: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</div>
);
}Explanation
useReducer(reducer, initialState) returns an array of [state, dispatch]. The reducer function must be pure: given the same state and action, it must always return the same new state, and it must not mutate the existing state object directly — always return a new object or array. Calling dispatch(action) triggers React to call the reducer with the current state and the dispatched action, then re-renders the component with the returned state. Actions conventionally have a type field describing what happened and an optional payload carrying any additional data needed for the update.
Cricket analogy: Calling dispatch({type:'wicket', payload:{batsman:'Kohli'}}) hands the scorer a description of what happened; the scorer never erases the old scoresheet, it always writes a brand-new line reflecting overs, runs, and wickets, then the board is redrawn.
Example
// useReducer with a lazy initializer and more complex state shape
function init(initialCount) {
return { count: initialCount, history: [] };
}
function reducer(state, action) {
switch (action.type) {
case 'add':
return {
count: state.count + action.payload,
history: [...state.history, action.payload],
};
case 'clear':
return init(0);
default:
return state;
}
}
function Ledger() {
const [state, dispatch] = useReducer(reducer, 0, init);
return (
<div>
<p>Total: {state.count}</p>
<button onClick={() => dispatch({ type: 'add', payload: 5 })}>Add 5</button>
<button onClick={() => dispatch({ type: 'clear' })}>Clear</button>
<ul>
{state.history.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
</div>
);
}Output
Each click on 'Add 5' dispatches an 'add' action, which the reducer uses to compute a new state object with an updated count and an appended history array; React re-renders Ledger with the fresh state, so the total and list update in the UI. Clicking 'Clear' resets state back to the initializer's output, { count: 0, history: [] }.
Cricket analogy: Each 'Add 5' tap on a manual run-counter dispatches an 'add' action that produces a new total and appends to the ball-by-ball history, updating the scoreboard; pressing 'Clear' resets everything back to 0 runs and an empty history, like starting a new innings.
Key Takeaways
- useReducer manages state via a pure reducer function and dispatched action objects, similar to Redux's core pattern.
- It is preferred over multiple useState calls when state updates are interdependent or when the next state depends on the previous state and an action.
- The reducer must never mutate state directly; it must always return a new state value.
- useReducer accepts an optional third argument, a lazy initializer function, for expensive or derived initial state.
- Pairing useReducer with Context API is a common lightweight alternative to external state management libraries.
Practice what you learned
1. What does useReducer return?
2. What is a strict requirement for the reducer function passed to useReducer?
3. In the action object convention used with useReducer, what does the 'type' field typically represent?
4. When is useReducer generally preferred over useState?
5. What is the purpose of the optional third argument to useReducer?
Was this page helpful?
You May Also Like
The useState Hook
Master useState, the Hook that adds local state to function components, including initial values, updater functions, and functional updates.
Context API for State Management
Learn how React's built-in Context API lets you share state across components without prop drilling.
Redux Basics
Learn the core Redux concepts—store, actions, reducers, and dispatch—and how Redux Toolkit simplifies them.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics