Questions
This component's state logic is complex. Refactor it with `useReducer`.
A developer has built a ShoppingCart component. The cart state includes a list of items (each with a productId, name, price, and quantity) and a totalPrice.
They are currently using multiple useState calls to manage these pieces of state. Adding, removing, or updating item quantities involves several setItems and setTotalPrice calls, making the logic spread out, hard to follow, and prone to inconsistencies.
You’ve been given the ShoppingCart component. Your task is to refactor its state management to use the useReducer hook.
- Centralize the cart’s state logic (adding, removing, updating quantity) into a single
cartReducerfunction. - Replace the multiple
useStatecalls with a singleuseReducercall. - Ensure the component’s functionality remains identical.
import React, { useState, useEffect } from 'react';
// Helper to calculate total price
const calculateTotal = (items) => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
};
export default function ShoppingCart() {
const [items, setItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
// Update total price whenever items change
useEffect(() => {
setTotalPrice(calculateTotal(items));
}, [items]);
const addItem = (product) => {
setItems(prevItems => {
const existingItem = prevItems.find(item => item.productId === product.id);
if (existingItem) {
return prevItems.map(item =>
item.productId === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prevItems, { productId: product.id, name: product.name, price: product.price, quantity: 1 }];
});
};
const removeItem = (productId) => {
setItems(prevItems => prevItems.filter(item => item.productId !== productId));
};
const updateQuantity = (productId, newQuantity) => {
setItems(prevItems =>
prevItems.map(item =>
item.productId === productId
? { ...item, quantity: newQuantity }
: item
)
);
};
return (
<div style={{ padding: '20px', border: '1px solid #ccc' }}>
<h1>Shopping Cart</h1>
<h2>Items</h2>
{items.length === 0 ? (
<p>Your cart is empty.</p>
) : (
<ul>
{items.map(item => (
<li key={item.productId}>
{item.name} (x{item.quantity}) - ${item.price * item.quantity}
<button onClick={() => updateQuantity(item.productId, item.quantity + 1)}>+</button>
<button onClick={() => updateQuantity(item.productId, item.quantity - 1)}>-</button>
<button onClick={() => removeItem(item.productId)}>Remove</button>
</li>
))}
</ul>
)}
<h3>Total: ${totalPrice.toFixed(2)}</h3>
<hr />
<h2>Products</h2>
<button onClick={() => addItem({ id: 1, name: 'Laptop', price: 1200 })}>Add Laptop</button>
<button onClick={() => addItem({ id: 2, name: 'Mouse', price: 25 })}>Add Mouse</button>
</div>
);
} How Different Experience Levels Approach This
import React, { useState, useEffect } from 'react'; // Helper to calculate total price const calculateTotal = (items) => { return items.reduce((sum, item) => sum + item.price * item.quantity, 0); }; export default function ShoppingCart() { const [items, setItems] = useState([]); const [totalPrice, setTotalPrice] = useState(0); // Update total price whenever items change useEffect(() => { setTotalPrice(calculateTotal(items)); }, [items]); const addItem = (product) => { setItems(prevItems => { const existingItem = prevItems.find(item => item.productId === product.id); if (existingItem) { return prevItems.map(item => item.productId === product.id ? { ...item, quantity: item.quantity + 1 } : item ); } return [...prevItems, { productId: product.id, name: product.name, price: product.price, quantity: 1 }]; }); }; const removeItem = (productId) => { setItems(prevItems => prevItems.filter(item => item.productId !== productId)); }; const updateQuantity = (productId, newQuantity) => { setItems(prevItems => prevItems.map(item => item.productId === productId ? { ...item, quantity: newQuantity } : item ) ); }; return ( <div style={{ padding: '20px', border: '1px solid #ccc' }}> <h1>Shopping Cart</h1> <h2>Items</h2> {items.length === 0 ? ( <p>Your cart is empty.</p> ) : ( <ul> {items.map(item => ( <li key={item.productId}> {item.name} (x{item.quantity}) - ${item.price * item.quantity} <button onClick={() => updateQuantity(item.productId, item.quantity + 1)}>+</button> <button onClick={() => updateQuantity(item.productId, item.quantity - 1)}>-</button> <button onClick={() => removeItem(item.productId)}>Remove</button> </li> ))} </ul> )} <h3>Total: ${totalPrice.toFixed(2)}</h3> <hr /> <h2>Products</h2> <button onClick={() => addItem({ id: 1, name: 'Laptop', price: 1200 })}>Add Laptop</button> <button onClick={() => addItem({ id: 2, name: 'Mouse', price: 25 })}>Add Mouse</button> </div> ); }
import React, { useReducer, useEffect } from 'react'; // 1. Define the initial state for the cart const initialCartState = { items: [], totalPrice: 0, }; // Helper to calculate total price (can be reused in reducer) const calculateTotal = (items) => { return items.reduce((sum, item) => sum + item.price * item.quantity, 0); }; // 2. Define the reducer function function cartReducer(state, action) { switch (action.type) { case 'ADD_ITEM': { const product = action.payload; const existingItem = state.items.find(item => item.productId === product.id); let updatedItems; if (existingItem) { updatedItems = state.items.map(item => item.productId === product.id ? { ...item, quantity: item.quantity + 1 } : item ); } else { updatedItems = [...state.items, { productId: product.id, name: product.name, price: product.price, quantity: 1 }]; } return { ...state, items: updatedItems, totalPrice: calculateTotal(updatedItems), }; } case 'REMOVE_ITEM': { const productId = action.payload; const updatedItems = state.items.filter(item => item.productId !== productId); return { ...state, items: updatedItems, totalPrice: calculateTotal(updatedItems), }; } case 'UPDATE_QUANTITY': { const { productId, newQuantity } = action.payload; const updatedItems = state.items.map(item => item.productId === productId ? { ...item, quantity: newQuantity } : item ); return { ...state, items: updatedItems, totalPrice: calculateTotal(updatedItems), }; } default: throw new Error(`Unhandled action type: ${action.type}`); } } export default function ShoppingCart() { // 3. Replace useState with useReducer const [cartState, dispatch] = useReducer(cartReducer, initialCartState); const { items, totalPrice } = cartState; // Action dispatchers const addItem = (product) => { dispatch({ type: 'ADD_ITEM', payload: product }); }; const removeItem = (productId) => { dispatch({ type: 'REMOVE_ITEM', payload: productId }); }; const updateQuantity = (productId, newQuantity) => { // Prevent quantity from going below 1, or remove if 0 if (newQuantity <= 0) { dispatch({ type: 'REMOVE_ITEM', payload: productId }); } else { dispatch({ type: 'UPDATE_QUANTITY', payload: { productId, newQuantity } }); } }; return ( <div style={{ padding: '20px', border: '1px solid #ccc' }}> <h1>Shopping Cart</h1> <h2>Items</h2> {items.length === 0 ? ( <p>Your cart is empty.</p> ) : ( <ul> {items.map(item => ( <li key={item.productId}> {item.name} (x{item.quantity}) - ${item.price * item.quantity} <button onClick={() => updateQuantity(item.productId, item.quantity + 1)}>+</button> <button onClick={() => updateQuantity(item.productId, item.quantity - 1)}>-</button> <button onClick={() => removeItem(item.productId)}>Remove</button> </li> ))} </ul> )} <h3>Total: ${totalPrice.toFixed(2)}</h3> <hr /> <h2>Products</h2> <button onClick={() => addItem({ id: 1, name: 'Laptop', price: 1200 })}>Add Laptop</button> <button onClick={() => addItem({ id: 2, name: 'Mouse', price: 25 })}>Add Mouse</button> </div> ); }
- Context over facts: Explains when and why, not just what
- Real examples: Provides specific use cases from production experience
- Trade-offs: Acknowledges pros, cons, and decision factors
Practice Question
When is useReducer generally preferred over useState?