React useReducer Hook: Managing Complex State
useReducer is ideal when your state logic is complex, involves multiple sub-values, or depends on previous state. Learn its syntax, roles, and practical points.
Introduction: Why useReducer?
The useReducer
hook in React centralizes complex state logic, helping when updates rely on previous state or when state structure is non-trivial. It's often used for forms, undo operations, or when state transitions are numerous.
What is useReducer? Why Use It
If your state is basic (a single value), useState
is fine. But if state needs coordinated changes (like a shopping cart, multi-step form, or undo/redo), useReducer
keeps all logic in one place with actions and a reducer function.
✅ Feature Comparison Table
Feature | useState | useReducer |
Simple logic | ✅ | ✅ |
Complex logic | ✅ | ✅ |
Multiple fields | ✅ | ✅ |
State depends on previous | ☐ | ✅ |
Encapsulated logic | ☐ | ✅ |
useReducer Syntax & Parameters
const [state, dispatch] = useReducer(reducer, initialArg, init);
- state: Current state value, managed by the reducer.
- dispatch: Function to send action objects to the reducer.
- reducer: Pure function that takes (state, action) and returns next state.
- initialArg: Starting state value, any type.
- init (optional): Lazy initializer for computing initial state (runs only once).
Analogy: Dispatching in Real Life
Imagine a dispatcher in a warehouse: they shout out an action (e.g., "add item"), the manager (reducer) checks the order and updates the inventory (state) accordingly.
Code Example: Complex Form State
const initialState = { name: '', age: '' };
function reducer(state, action) {
switch (action.type) {
case 'SET_NAME': return { ...state, name: action.payload };
case 'SET_AGE': return { ...state, age: action.payload };
default: throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState);
Explanation
This reducer manages multiple form fields in a single object, making it much easier to maintain than separate useState
calls.
Advanced: Lazy Initialization
function init(arg) { return { count: arg }; }
function reducer(state, action) {
switch(action.type) {
case 'increment': return { count: state.count + 1 };
case 'reset': return init(action.payload);
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, 0, init);
Use Case:
By passing init
, you can compute the initial state once on mount, or reset to a new value efficiently, such as from props or localStorage.
Best Practices
- Keep reducer pure (no async or side-effects inside).
- Use action objects with
type
andpayload
for flexibility. - Avoid direct state mutations; always return a new state object.
- Break out reducer function for clarity and reusability.
- Use lazy initializer for performance when computing expensive initial state.
Comparison: useReducer vs Redux
Both useReducer
and Redux use reducer functions, actions, and dispatch, but Redux is for global app state, while useReducer
is for local, component state.
Real-World Example: Shopping List
function shoppingReducer(state, action) {
switch(action.type) {
case 'ADD_ITEM': return [...state, action.payload];
case 'REMOVE_ITEM': return state.filter((item) => item.id !== action.payload);
default: return state;
}
}
const [items, dispatch] = useReducer(shoppingReducer, []);
Use Case:
This manages a dynamic list with complex add/removal logic, scaling much better than useState
as your app grows.
📊 Table: Key Concepts
Prop | Role | Best Practice |
state | Current value | Return new object, never mutate |
dispatch | Sends actions | Always use React dispatch |
reducer | Updates state | Pure logic only |
initialArg | Initial value | Pass object/array for complex state |
init | Lazy setup | Use for optimized initialization |