Overview
The order slice manages the current shopping cart, order history, and checkout flow. It implements automatic discount calculation (10% off for 3+ of the same pizza) and persists both current orders and order history to localStorage.
State Shape
interface OrderState {
currentOrder: OrderItem[];
orderHistory: Order[];
loading: boolean;
}
Array of items in the current shopping cart. Each item includes the pizza, quantity, and calculated pricing.
Array of completed orders with full details including timestamps and totals.
Loading state indicator for async operations.
Initial State
The initial state is hydrated from localStorage:
{
currentOrder: [], // Loaded from localStorage.pizza_current_order
orderHistory: [], // Loaded from localStorage.pizza_orders
loading: false
}
Persistence
- Current order: Stored in
localStorage under key pizza_current_order
- Order history: Stored in
localStorage under key pizza_orders
- Current order is automatically persisted after add, remove, and update operations
- Order history is updated when orders are completed
Actions
setLoading
Sets the loading state for async operations.
setLoading(loading: boolean): void
Example Usage:
import { useDispatch } from 'react-redux';
import { setLoading } from './store/orderSlice';
const CheckoutButton = () => {
const dispatch = useDispatch();
const handleCheckout = async () => {
dispatch(setLoading(true));
try {
await processOrder();
} finally {
dispatch(setLoading(false));
}
};
return <button onClick={handleCheckout}>Checkout</button>;
};
Side Effects:
addToOrder
Adds a pizza to the current order or increases quantity if already present. Automatically calculates discounts and totals.
addToOrder(payload: { pizza: Pizza; quantity: number }): void
The pizza object to add to the order.
Number of pizzas to add. Must be a positive integer.
Example Usage:
import { useDispatch } from 'react-redux';
import { addToOrder } from './store/orderSlice';
import type { Pizza } from './types';
const PizzaCard = ({ pizza }: { pizza: Pizza }) => {
const dispatch = useDispatch();
const [quantity, setQuantity] = useState(1);
const handleAddToCart = () => {
dispatch(addToOrder({ pizza, quantity }));
};
return (
<div>
<h3>{pizza.name}</h3>
<p>${pizza.price}</p>
<input
type="number"
min="1"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
/>
<button onClick={handleAddToCart}>Add to Cart</button>
</div>
);
};
Reducer Logic:
- Searches for existing item with matching
pizza.id
- If found: Increases quantity and recalculates totals
- If not found: Creates new order item with calculated totals
- Applies automatic discount calculation
- Persists updated order to localStorage
Side Effects:
- Updates or adds item to
state.currentOrder
- Recalculates
originalLinePrice, discountAmount, and finalLineTotal
- Persists entire current order to
localStorage.pizza_current_order
Discount Logic:
- 10% discount applied when quantity ≥ 3 for the same pizza
- Discount calculated as:
originalLinePrice * 0.1
- Final total:
originalLinePrice - discountAmount
removeFromOrder
Removes a pizza from the current order by pizza ID.
removeFromOrder(pizzaId: string): void
The ID of the pizza to remove from the order.
Example Usage:
import { useDispatch } from 'react-redux';
import { removeFromOrder } from './store/orderSlice';
const CartItem = ({ item }: { item: OrderItem }) => {
const dispatch = useDispatch();
const handleRemove = () => {
dispatch(removeFromOrder(item.pizza.id));
};
return (
<div>
<span>{item.pizza.name} x{item.quantity}</span>
<button onClick={handleRemove}>Remove</button>
</div>
);
};
Side Effects:
- Filters out the item with matching
pizza.id from state.currentOrder
- Persists updated order to
localStorage.pizza_current_order
updateQuantity
Updates the quantity of a specific pizza in the order and recalculates totals.
updateQuantity(payload: { pizzaId: string; quantity: number }): void
The ID of the pizza to update.
New quantity for the pizza. Must be a positive integer.
Example Usage:
import { useDispatch } from 'react-redux';
import { updateQuantity } from './store/orderSlice';
const CartItem = ({ item }: { item: OrderItem }) => {
const dispatch = useDispatch();
const handleQuantityChange = (newQuantity: number) => {
if (newQuantity > 0) {
dispatch(updateQuantity({
pizzaId: item.pizza.id,
quantity: newQuantity
}));
}
};
return (
<div>
<span>{item.pizza.name}</span>
<button onClick={() => handleQuantityChange(item.quantity - 1)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => handleQuantityChange(item.quantity + 1)}>+</button>
</div>
);
};
Reducer Logic:
- Finds the order item by
pizzaId
- Updates the quantity
- Recalculates totals using
calculateItemTotals
- Applies discount if quantity ≥ 3
- Updates the item in place
Side Effects:
- Updates
quantity for the matching item in state.currentOrder
- Recalculates
originalLinePrice, discountAmount, and finalLineTotal
- Persists updated order to
localStorage.pizza_current_order
clearOrder
Clears the current order completely.
Example Usage:
import { useDispatch } from 'react-redux';
import { clearOrder } from './store/orderSlice';
const ClearCartButton = () => {
const dispatch = useDispatch();
const handleClear = () => {
if (confirm('Clear your cart?')) {
dispatch(clearOrder());
}
};
return <button onClick={handleClear}>Clear Cart</button>;
};
Side Effects:
- Sets
state.currentOrder to empty array
- Removes
pizza_current_order from localStorage
addOrderToHistory
Completes the current order by adding it to history and clearing the cart.
addOrderToHistory(order: Order): void
Complete order object to add to history. Must include id, items, subtotal, totalDiscount, finalTotal, and timestamp.
Example Usage:
import { useDispatch, useSelector } from 'react-redux';
import { addOrderToHistory } from './store/orderSlice';
import type { Order } from './types';
import type { RootState } from './store';
const CheckoutButton = () => {
const dispatch = useDispatch();
const currentOrder = useSelector((state: RootState) => state.order.currentOrder);
const handleCheckout = () => {
// Calculate totals
const subtotal = currentOrder.reduce((sum, item) => sum + item.originalLinePrice, 0);
const totalDiscount = currentOrder.reduce((sum, item) => sum + item.discountAmount, 0);
const finalTotal = currentOrder.reduce((sum, item) => sum + item.finalLineTotal, 0);
const order: Order = {
id: `order-${Date.now()}`,
items: currentOrder,
subtotal,
totalDiscount,
finalTotal,
timestamp: new Date().toISOString()
};
dispatch(addOrderToHistory(order));
};
return <button onClick={handleCheckout}>Complete Order</button>;
};
Reducer Logic:
- Appends order to
state.orderHistory
- Clears
state.currentOrder
- Persists order history to localStorage
- Removes current order from localStorage
Side Effects:
- Adds order to
state.orderHistory
- Clears
state.currentOrder
- Persists order history to
localStorage.pizza_orders
- Removes
pizza_current_order from localStorage
Helper Functions
calculateItemTotals
Internal helper function that calculates pricing for an order item.
const calculateItemTotals = (item: { pizza: Pizza; quantity: number }): OrderItem => {
const originalLinePrice = item.pizza.price * item.quantity;
let discountAmount = 0;
// Discount: 10% for 3+ items of the same pizza
if (item.quantity >= 3) {
discountAmount = originalLinePrice * 0.1;
}
const finalLineTotal = originalLinePrice - discountAmount;
return {
...item,
originalLinePrice,
discountAmount,
finalLineTotal,
};
};
Calculation Details:
originalLinePrice: pizza.price * quantity
discountAmount: originalLinePrice * 0.1 if quantity >= 3, else 0
finalLineTotal: originalLinePrice - discountAmount
Example:
// Pizza costs $20, ordering 4 pizzas
const item = {
pizza: { price: 20, ... },
quantity: 4
};
const result = calculateItemTotals(item);
// {
// originalLinePrice: 80, // 20 * 4
// discountAmount: 8, // 80 * 0.1 (10% off for 3+)
// finalLineTotal: 72 // 80 - 8
// }
Selectors
Common selector patterns for order state:
import { RootState } from './store';
// Get current order
const selectCurrentOrder = (state: RootState) => state.order.currentOrder;
// Get order history
const selectOrderHistory = (state: RootState) => state.order.orderHistory;
// Get cart totals
const selectCartTotals = (state: RootState) => {
const items = state.order.currentOrder;
return {
subtotal: items.reduce((sum, item) => sum + item.originalLinePrice, 0),
totalDiscount: items.reduce((sum, item) => sum + item.discountAmount, 0),
finalTotal: items.reduce((sum, item) => sum + item.finalLineTotal, 0),
itemCount: items.reduce((sum, item) => sum + item.quantity, 0)
};
};
// Get cart item count
const selectCartItemCount = (state: RootState) =>
state.order.currentOrder.reduce((sum, item) => sum + item.quantity, 0);
Discount Rules
The order slice implements automatic discount calculation:
- Threshold: 3 or more of the same pizza
- Discount: 10% off the line total
- Application: Automatically applied in
addToOrder and updateQuantity
- Calculation: Applied per line item, not to the entire order
Example Scenarios:
// Scenario 1: 2 Margherita pizzas ($15 each)
// No discount (quantity < 3)
// Original: $30, Discount: $0, Final: $30
// Scenario 2: 3 Margherita pizzas ($15 each)
// 10% discount applied
// Original: $45, Discount: $4.50, Final: $40.50
// Scenario 3: 5 Pepperoni pizzas ($18 each)
// 10% discount applied
// Original: $90, Discount: $9, Final: $81