useReducer Hook
Overview
useState is perfect for simple, independent variables (like a string or boolean). However, as components grow, state often becomes highly complex. You might have an object with 10 different properties, where updating one property requires complex math or depends on the values of the other 9 properties.
When useState logic becomes spaghetti, developers reach for `useReducer`.
useReducer is an alternative to useState based on the Redux pattern. Instead of calling setter functions directly (setScore(5)), you dispatch Actions (dispatch({ type: 'SCORE_GOAL' })). These actions are intercepted by a centralized Reducer Function.
The Reducer is a pure JavaScript function that takes the current state and the action, looks at the action type in a switch statement, runs the complex logic, and returns the brand new state. This completely decouples your complex state logic from your UI rendering logic, making it infinitely easier to test and maintain.
Syntax
import { useReducer } from 'react';
// 1. Define the Reducer Function (OUTSIDE the component!)
// It takes the current state, and the action we dispatched.
function counterReducer(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; // Always return state if action is unknown
}
}
function App() {
// 2. Initialize the hook with the reducer function and a starting state
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<h1>{state.count}</h1>
{/* 3. Dispatch 'Actions' (objects with a 'type' property) */}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}Common Pitfalls
- Mutating state in the reducer: Just like
useState, you CANNOT mutate the state inside a reducer. Writingcase 'INC': state.count++; return state;will break React. You must ALWAYS return a brand new object:return { ...state, count: state.count + 1 }.
Interview Questions
useReducer over useState?You should use useReducer when state logic is complex, involves multiple sub-values, or when the next state depends heavily on the previous state. It is also beneficial when you need to pass a state-updating function deep down a component tree, as passing dispatch is cleaner than passing 5 different setX functions.
Because the reducer is a pure function that does not rely on any component props or variables, defining it outside the component prevents it from being unnecessarily recreated in memory every single time the component re-renders.
Real-World Example
Complex Form State: By moving the logic into a reducer, the handleSubmit function simply 'narrates' what is happening (SUBMIT_START, SUBMIT_SUCCESS). It doesn't have to worry about manually toggling booleans or clearing errors; the reducer handles all of that complex orchestration.
// Initial State for a checkout form
const initialForm = {
name: '', email: '', isSubmitting: false, error: null
};
// Centralized logic hub
function formReducer(state, action) {
switch (action.type) {
case 'TYPE':
return { ...state, [action.field]: action.value, error: null };
case 'SUBMIT_START':
return { ...state, isSubmitting: true, error: null };
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false };
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false, error: action.payload };
default:
return state;
}
}
function Checkout() {
const [state, dispatch] = useReducer(formReducer, initialForm);
const handleSubmit = async () => {
dispatch({ type: 'SUBMIT_START' });
try {
await api.submit(state.name, state.email);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (err) {
// We attach data to the action using the 'payload' property
dispatch({ type: 'SUBMIT_ERROR', payload: err.message });
}
};
// ... JSX ...
}Check Your Knowledge
Test your understanding of useReducer Hook with these quick questions.