The useReducer hook is an alternative to useState for managing complex state logic. It's especially useful when the next state depends on the previous state or when state transitions are complex.
Syntax
const [state, dispatch] = useReducer(reducer, initialState, init);
Example: Shopping Cart
import React, { useReducer } from 'react';
// Reducer function
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
// Check if item already exists
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
// Update quantity of existing item
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
total: state.total + action.payload.price
};
} else {
// Add new item
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
total: state.total + action.payload.price
};
}
case 'REMOVE_ITEM':
const itemToRemove = state.items.find(item => item.id === action.payload);
if (!itemToRemove) return state;
// If quantity is 1, remove the item completely
if (itemToRemove.quantity === 1) {
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
total: state.total - itemToRemove.price
};
} else {
// Otherwise decrease quantity
return {
...state,
items: state.items.map(item =>
item.id === action.payload
? { ...item, quantity: item.quantity - 1 }
: item
),
total: state.total - itemToRemove.price
};
}
case 'CLEAR_CART':
return {
items: [],
total: 0
};
default:
return state;
}
}
// Component using the reducer
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
// Sample products
const products = [
{ id: 1, name: 'Product 1', price: 10.99 },
{ id: 2, name: 'Product 2', price: 24.99 },
{ id: 3, name: 'Product 3', price: 5.99 }
];
return (
<div>
<h2>Shopping Cart</h2>
<div className="products">
<h3>Products</h3>
{products.map(product => (
<div key={product.id} style={{ margin: '10px 0', padding: '10px', border: '1px solid #eee' }}>
<p>{product.name} - ${product.price.toFixed(2)}</p>
<button
onClick={() => dispatch({
type: 'ADD_ITEM',
payload: product
})}
>
Add to Cart
</button>
</div>
))}
</div>
<div className="cart" style={{ marginTop: '20px' }}>
<h3>Cart Items</h3>
{cart.items.length === 0 ? (
<p>Your cart is empty</p>
) : (
<>
{cart.items.map(item => (
<div key={item.id} style={{ margin: '10px 0', padding: '10px', border: '1px solid #eee' }}>
<p>
{item.name} - ${item.price.toFixed(2)} x {item.quantity} = ${(item.price * item.quantity).toFixed(2)}
</p>
<button
onClick={() => dispatch({
type: 'REMOVE_ITEM',
payload: item.id
})}
>
Remove One
</button>
</div>
))}
<div style={{ marginTop: '20px', fontWeight: 'bold' }}>
Total: ${cart.total.toFixed(2)}
</div>
<button
onClick={() => dispatch({ type: 'CLEAR_CART' })}
style={{ marginTop: '10px' }}
>
Clear Cart
</button>
</>
)}
</div>
</div>
);
}
Key Points
- The reducer function takes
(state, action) and returns the new state
- The dispatch function is used to send actions to the reducer
- Actions are typically objects with a
type property and optional payload
- The optional
init function can be used to lazily create the initial state
- Similar to Redux but scoped to a component
Best Practices
- Use for complex state logic with multiple sub-values or when next state depends on previous state
- Keep reducers pure - no side effects, API calls, or mutations
- Use action constants to avoid typos
- Consider combining with
useContext for global state management
- Split large reducers into smaller functions for maintainability