useState vs. useReducer : The Tool to Choose When State Gets Complex

Chris is building a shopping cart feature for an e-commerce site. At first, it was simple. A single array to hold the product list was enough.
const [cart, setCart] = useState<Product[]>([]);But then, the requirements started piling up.
Before he knew it, Chris's component was plastered with useState calls, and the code for updating state was scattered everywhere. Bugs started popping up, like forgetting to turn off the loading state or missing an error reset.
"Why is state management so complicated? Can't I handle this all in one place?"
This is the exact moment to pull out useReducer.
1. The Limitations of useState: Scattered Logic
useState is best for handling independent values (numbers, strings, simple booleans). However, problems arise when multiple states change dependently on each other.
// ❌ Bad Example: State logic is hidden inside event handlers
const handleAddProduct = (product) => {
setIsLoading(true);
setError(null);
api.addToCart(product)
.then((newCart) => {
setCart(newCart);
setTotal(calculateTotal(newCart)); // Total calculation logic is here...
setIsLoading(false);
})
.catch((err) => {
setError(err.message);
setIsLoading(false);
});
};
const handleDeleteProduct = (id) => {
// We have to write total calculation logic again when deleting (Duplication!)
// ...
};The logic for "what to change and how" is fragmented across various functions inside the component. Anyone reading this code has to play hide-and-seek to find every setCart call.
2. useReducer: The Central Control Room for State
useReducer is a tool that separates state update logic from the component. Simply put, it's like an "accounting ledger" system.
Let's refactor Chris's shopping cart code using useReducer.
Step 1: Define Action and State (TypeScript)
First, define "what can happen."
// types.ts
type CartState = {
cart: Product[];
total: number;
isLoading: boolean;
error: string | null;
};
// List all possible events (Actions)
type CartAction =
| { type: 'ADD_START' }
| { type: 'ADD_SUCCESS'; payload: Product[] }
| { type: 'ADD_FAIL'; payload: string }
| { type: 'DELETE_ITEM'; payload: number }; // idStep 2: Write the Reducer Function (Centralized Logic)
All state change logic gathers in this single function.
// cartReducer.ts
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_START':
return { ...state, isLoading: true, error: null };
case 'ADD_SUCCESS':
const newCart = action.payload;
return {
...state,
isLoading: false,
cart: newCart,
total: calculateTotal(newCart) // Calculation logic lives here, once!
};
case 'ADD_FAIL':
return { ...state, isLoading: false, error: action.payload };
default:
return state;
}
}Step 3: Use in Component (Dispatch)
Now, the component doesn't need to know the logic. It only needs to send a dispatch (request).
// CartComponent.tsx
function CartComponent() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const handleAddProduct = (product) => {
dispatch({ type: 'ADD_START' }); // Report "Started"
api.addToCart(product)
.then(res => dispatch({ type: 'ADD_SUCCESS', payload: res }))
.catch(err => dispatch({ type: 'ADD_FAIL', payload: err.message }));
};
return (
<div>
{state.isLoading && <Spinner />}
{state.error && <ErrorMsg msg={state.error} />}
</div>
);
}3. When Should You Use Which?
So, is useReducer always better? No. It has the disadvantage of requiring more boilerplate code. Let's establish clear criteria.
| Criteria | useState | useReducer |
| Data Type | Primitives (number, string, boolean) | Complex Objects, Arrays |
| State Count | 1~2 independent states | Multiple related states |
| Logic Location | Inside event handlers | Outside component (reducer function) |
| Dependency | Not dependent on previous state | Next state depends heavily on previous state |
✅ Chris's Selection Criteria
Key Takeaways
Now that we know how to manage state, it's time to handle Forms that interact with users. There are two ways to handle input; which side are you on?
Continuing in: "Controlled vs. Uncontrolled Components: The Secret to How React Hook Form Reduces Rendering."