JAVASCRIPT
Advanced State Management with useReducer Hook
Learn to manage complex application state more predictably with React's `useReducer` hook. This snippet illustrates building a simple shopping cart with multiple actions (add, remove, update quantity).
import React, { useReducer } from 'react';
const initialState = {
cart: [],
totalItems: 0,
totalPrice: 0,
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
{
const existingItemIndex = state.cart.findIndex(item => item.id === action.payload.id);
if (existingItemIndex > -1) {
const updatedCart = state.cart.map((item, index) =>
index === existingItemIndex
? { ...item, quantity: item.quantity + 1 }
: item
);
return {
...state,
cart: updatedCart,
totalItems: state.totalItems + 1,
totalPrice: state.totalPrice + action.payload.price,
};
} else {
return {
...state,
cart: [...state.cart, { ...action.payload, quantity: 1 }],
totalItems: state.totalItems + 1,
totalPrice: state.totalPrice + action.payload.price,
};
}
}
case 'REMOVE_ITEM':
{
const itemToRemove = state.cart.find(item => item.id === action.payload.id);
if (!itemToRemove) return state;
return {
...state,
cart: state.cart.filter(item => item.id !== action.payload.id),
totalItems: state.totalItems - itemToRemove.quantity,
totalPrice: state.totalPrice - (itemToRemove.price * itemToRemove.quantity),
};
}
case 'UPDATE_QUANTITY':
{
const updatedCart = state.cart.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
);
const newTotalItems = updatedCart.reduce((sum, item) => sum + item.quantity, 0);
const newTotalPrice = updatedCart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return {
...state,
cart: updatedCart,
totalItems: newTotalItems,
totalPrice: newTotalPrice,
};
}
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const products = [
{ id: 'p1', name: 'Laptop', price: 1200 },
{ id: 'p2', name: 'Mouse', price: 25 },
{ id: 'p3', name: 'Keyboard', price: 75 },
];
return (
<div>
<h2>Shopping Cart ({state.totalItems} items)</h2>
<h3>Total Price: ${state.totalPrice.toFixed(2)}</h3>
<div>
<h4>Available Products:</h4>
{products.map(product => (
<div key={product.id}>
{product.name} (${product.price.toFixed(2)})
<button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
Add to Cart
</button>
</div>
))}
</div>
<hr />
<h4>Your Cart:</h4>
{state.cart.length === 0 ? (
<p>Cart is empty.</p>
) : (
<ul>
{state.cart.map(item => (
<li key={item.id}>
{item.name} x {item.quantity} (${(item.price * item.quantity).toFixed(2)})
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: { id: item.id } })}>
Remove All
</button>
<button onClick={() => dispatch({ type: 'UPDATE_QUANTITY', payload: { id: item.id, quantity: Math.max(1, item.quantity - 1) } })}>
-1
</button>
<button onClick={() => dispatch({ type: 'UPDATE_QUANTITY', payload: { id: item.id, quantity: item.quantity + 1 } })}>
+1
</button>
</li>
))}
</ul>
)}
</div>
);
}
export default ShoppingCart;
How it works: This snippet demonstrates managing complex state logic using the `useReducer` hook. Instead of multiple `useState` calls, a single `cartReducer` function handles all state transitions for a shopping cart, based on dispatched `action` objects. This makes state updates more predictable, centralized, and easier to test, especially when the next state depends on the previous state or involves multiple related values like `cart` items, `totalItems`, and `totalPrice`.