Hooks
useReducer
Master the useReducer Hook, covering state machines, reducer pure functions, action dispatching, complex state containers, and useState comparisons.
1. Learning Objectives
In this lesson, you will master React's hook for complex state management. By the end of this topic, you will be able to:
- Differentiate when to use
useStateversususeReducer. - Write pure reducer functions to manage state transitions.
- Dispatch actions containing type identifiers and payloads.
- Model complex state objects and state machines cleanly.
- Avoid mutating state inside reducer bodies.
2. Overview
The useReducer Hook is an alternative to useState used for managing complex state structures, or state updates that depend on other state properties. It models state updates using a **unidirectional flow**: components dispatch actions describing what happened, and a pure **reducer function** computes the next state.
3. Why This Topic Matters
Using `useReducer` keeps state updates predictable and organized:
- Complex State Structures: When a component's state contains multiple nested fields (like user preferences, shopping carts, and input fields), writing multiple `useState` setters becomes difficult to manage. `useReducer` groups all state updates in a single function.
- Separation of Concerns: Grouping all state update logic in a separate reducer function separates layout rendering logic from business logic.
4. Real-World Analogy
Think of useReducer like **ordering from a cafe kitchen**:
- useState (Self-Service): You go into the kitchen and prepare the sandwich yourself: "Get bread, add ham, add cheese, toast." You directly manage each step.
- useReducer (Cafe Order): You look at the menu (actions), select an item, and hand the order ticket (the action object) to the cashier (dispatch). The chef (the reducer function) reads the order and prepares the sandwich using the cafe's standard recipe. The chef handles the steps, and you simply receive the sandwich.
5. Core Concepts
Below is a comparison of `useState` and `useReducer` use cases:
| Feature | useState | useReducer |
|---|---|---|
| State Structure | Simple values (numbers, strings, flat arrays). | Complex, nested objects or state machines. |
| Update Logic Location | Declared inline inside component click handlers. | Centralized in a separate, pure reducer function. |
| Dependent State Updates | Requires multiple state setters, risking race conditions. | Updates multiple fields atomically in the reducer. |
| Testing | Tested by rendering components. | Easy to unit test as a pure JavaScript function. |
6. Syntax & API Reference
This example shows how to declare actions and define reducer functions in React:
7. Visual Diagram
This diagram displays the unidirectional data flow cycle inside the `useReducer` Hook:
8. Live Example — Full Working Code
Below is a complete HTML page demonstrating a Todo List built using `useReducer`:
What just happened? Clicking a task dispatches a TOGGLE_TODO action, passing the task's ID as the payload. Clicking the delete cross dispatches a REMOVE_TODO action. The todoReducer function processes these actions, returning a new state array reference to trigger a clean re-render.
9. Interactive Playground
Try It Yourself Challenges:
- Add a new action type named
CLEAR_COMPLETEDto filter out and remove all tasks that are marked as completed. - Modify the reducer to throw an error if an unhandled action type is dispatched, helping you catch typos.
10. Common Mistakes
| Mistake | Why it happens | Wrong | Correct |
|---|---|---|---|
| Mutating state inside the reducer function | Reducers must be pure functions. Mutating state arguments directly prevents React from detecting the change, so the UI fails to update. | case 'add': |
case 'add': (returns a new array reference) |
| Omitting default return cases in reducer switch statements | If an action type is dispatched that does not match any of the switch cases, omitting the default case can cause the reducer to return undefined, breaking the state. |
switch(action.type) { (missing default) |
default: (or throw an error to catch unhandled action types) |
11. Best Practices
- Keep reducers pure: Reducers must be pure, deterministic functions. Never make API calls, run side-effects, or generate random values (like `Math.random()`) inside a reducer.
- Use descriptive action types: Use uppercase strings for action types (e.g. `'ADD_TODO'`, `'REMOVE_TODO'`) to make them easy to read.
- Match standard action schemas: Standardize your action structures using `{ type, payload }` schemas.
- Throw on invalid action types: Throw an error in the default case of your reducer (e.g. `throw new Error()`) to catch typos in action types during development.
12. Browser Compatibility/Requirements
The useReducer Hook is supported in all browsers that run React.
13. Interview Questions
Q1: When should you use `useReducer` instead of `useState` inside a component?
Answer: Use `useReducer` when: (1) state updates are complex or depend on multiple nested values, (2) the next state depends on the previous state in complex ways, (3) updating one state field requires updating other fields atomically, or (4) you want to separate state update logic from UI rendering logic.
Q2: Why must a reducer function be pure? What happens if you run side-effects (like fetch requests) inside a reducer?
Answer: Reducers must be pure to ensure state updates are predictable and deterministic. Since React can execute reducers multiple times during rendering optimizations, running side-effects (like fetch requests or timers) inside a reducer would cause them to run repeatedly, leading to duplicate API requests, memory leaks, and unpredictable UI behavior. Side-effects must be handled inside event handlers or `useEffect` Hooks instead.
14. Debugging Exercise
Identify the bug in this reducer that prevents new items from appearing in the list:
Diagnosis: The reducer mutates the state array directly using `state.push()`. Since the array reference does not change, React determines that the state is unchanged and skips the re-render cycle.
Fix: Return a new array reference using the spread operator:
15. Practice Exercises
Exercise 1: Build a shopping cart reducer
Build a component named ShoppingCart. Define actions for ADD_PRODUCT, REMOVE_PRODUCT, and UPDATE_QUANTITY, and manage the shopping cart array using `useReducer`.
16. Scenario-Based Challenge
The Multi-Step Form Wizard Reducer:
You are designing a multi-step checkout form (shipping details, payment options, order review). If the user leaves the page or changes their inputs, you must validate and update fields atomically. Describe how you would use useReducer to manage the form state, and how you would handle resetting fields if validation fails.
17. Quick Quiz
Q1: Which method is called to send action objects to a reducer function?
A) dispatch()
B) submit()
C) execute()
Answer: A — dispatch() is the function used to send action objects to the reducer, triggering a state update cycle.
18. Summary & Key Takeaways
- The
useReducerHook is used to manage complex component state structures. - Reducers are pure functions that compute the next state based on the current state and an action.
- Components dispatch action objects containing `type` and `payload` properties to trigger state updates.
- Never mutate state directly inside a reducer; always return a new state reference.
19. Cheat Sheet
| useReducer API Element | Purpose / Description |
|---|---|
const [state, dispatch] = useReducer(reducer, initial); |
Initializes a state reducer and dispatch function. |
dispatch({ type: 'ADD', payload: data }) |
Dispatches an action to trigger a state update. |
function reducer(state, action) { ... } |
Computes the next state based on the action type. |