Appearance
购物车应用
学习目标
通过本章实战项目,你将综合运用:
- 复杂State管理策略
- 数组操作的最佳实践
- 数据计算与派生状态
- 组件组合与拆分
- 性能优化技巧
- 用户体验优化
- 表单验证与处理
- React 19新特性应用
项目概述
购物车是电商应用的核心功能,涵盖了React开发的多个重要概念:
- 商品列表展示
- 购物车状态管理
- 数量增减控制
- 价格计算
- 优惠券处理
- 本地存储
- 动画交互
我们将从基础版本逐步完善到生产级应用。
第一部分:基础购物车
1.1 项目结构
shopping-cart/
├── components/
│ ├── ProductList.jsx
│ ├── ProductCard.jsx
│ ├── Cart.jsx
│ ├── CartItem.jsx
│ └── CartSummary.jsx
├── hooks/
│ ├── useCart.js
│ └── useLocalStorage.js
├── utils/
│ └── calculations.js
└── App.jsx1.2 基础实现
jsx
import { useState, useMemo } from 'react';
function BasicShoppingCart() {
// 商品数据
const [products] = useState([
{ id: 1, name: 'iPhone 15 Pro', price: 7999, image: '/iphone.jpg', stock: 10 },
{ id: 2, name: 'MacBook Pro M3', price: 14999, image: '/macbook.jpg', stock: 5 },
{ id: 3, name: 'AirPods Pro', price: 1899, image: '/airpods.jpg', stock: 20 },
{ id: 4, name: 'iPad Air', price: 4799, image: '/ipad.jpg', stock: 15 },
{ id: 5, name: 'Apple Watch', price: 2999, image: '/watch.jpg', stock: 8 }
]);
// 购物车数据
const [cartItems, setCartItems] = useState([]);
// 添加到购物车
const addToCart = (product) => {
setCartItems(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
// 已存在,增加数量
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
// 不存在,添加新项
return [...prev, { ...product, quantity: 1 }];
});
};
// 从购物车移除
const removeFromCart = (productId) => {
setCartItems(prev => prev.filter(item => item.id !== productId));
};
// 更新数量
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId);
return;
}
setCartItems(prev =>
prev.map(item =>
item.id === productId ? { ...item, quantity } : item
)
);
};
// 清空购物车
const clearCart = () => {
setCartItems([]);
};
// 计算总价
const total = useMemo(() => {
return cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}, [cartItems]);
return (
<div className="shopping-cart-app">
<header>
<h1>购物车应用</h1>
<div className="cart-badge">
购物车 ({cartItems.length})
</div>
</header>
<div className="main-content">
{/* 商品列表 */}
<section className="products-section">
<h2>商品列表</h2>
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">¥{product.price}</p>
<p className="stock">库存: {product.stock}</p>
<button
onClick={() => addToCart(product)}
disabled={product.stock === 0}
className="add-to-cart-btn"
>
加入购物车
</button>
</div>
))}
</div>
</section>
{/* 购物车 */}
<section className="cart-section">
<h2>购物车</h2>
{cartItems.length === 0 ? (
<div className="empty-cart">
<p>购物车是空的</p>
<p>快去选购商品吧!</p>
</div>
) : (
<>
<div className="cart-items">
{cartItems.map(item => (
<div key={item.id} className="cart-item">
<img src={item.image} alt={item.name} />
<div className="item-info">
<h4>{item.name}</h4>
<p className="item-price">¥{item.price}</p>
</div>
<div className="quantity-controls">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="qty-btn"
>
-
</button>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, Number(e.target.value))}
min="1"
max={item.stock}
className="qty-input"
/>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
disabled={item.quantity >= item.stock}
className="qty-btn"
>
+
</button>
</div>
<div className="item-subtotal">
<p>小计</p>
<p className="subtotal-price">
¥{(item.price * item.quantity).toFixed(2)}
</p>
</div>
<button
onClick={() => removeFromCart(item.id)}
className="remove-btn"
>
删除
</button>
</div>
))}
</div>
<div className="cart-summary">
<div className="summary-row">
<span>商品总价</span>
<span className="total-price">¥{total.toFixed(2)}</span>
</div>
<div className="cart-actions">
<button onClick={clearCart} className="clear-btn">
清空购物车
</button>
<button className="checkout-btn">
去结算
</button>
</div>
</div>
</>
)}
</section>
</div>
</div>
);
}
export default BasicShoppingCart;第二部分:增强功能
2.1 添加优惠券功能
jsx
function CartWithCoupon() {
const [products] = useState([/* ... */]);
const [cartItems, setCartItems] = useState([]);
const [couponCode, setCouponCode] = useState('');
const [appliedCoupon, setAppliedCoupon] = useState(null);
// 优惠券数据库
const coupons = {
'SAVE10': { type: 'percentage', value: 0.1, minAmount: 0 },
'SAVE20': { type: 'percentage', value: 0.2, minAmount: 1000 },
'SAVE100': { type: 'fixed', value: 100, minAmount: 500 },
'FREESHIP': { type: 'shipping', value: 0, minAmount: 0 }
};
// 应用优惠券
const applyCoupon = () => {
const coupon = coupons[couponCode.toUpperCase()];
if (coupon) {
if (subtotal >= coupon.minAmount) {
setAppliedCoupon({ code: couponCode, ...coupon });
} else {
alert(`此优惠券需要满 ¥${coupon.minAmount} 才能使用`);
}
} else {
alert('优惠券无效');
}
};
// 移除优惠券
const removeCoupon = () => {
setAppliedCoupon(null);
setCouponCode('');
};
// 计算小计
const subtotal = useMemo(() => {
return cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
}, [cartItems]);
// 计算优惠金额
const discount = useMemo(() => {
if (!appliedCoupon) return 0;
if (appliedCoupon.type === 'percentage') {
return subtotal * appliedCoupon.value;
} else if (appliedCoupon.type === 'fixed') {
return appliedCoupon.value;
}
return 0;
}, [appliedCoupon, subtotal]);
// 计算运费
const shipping = useMemo(() => {
if (appliedCoupon?.type === 'shipping') return 0;
if (subtotal >= 299) return 0;
return 20;
}, [subtotal, appliedCoupon]);
// 计算总价
const total = subtotal - discount + shipping;
return (
<div className="shopping-cart">
{/* 商品列表 */}
<ProductList products={products} onAddToCart={addToCart} />
{/* 购物车 */}
<div className="cart">
<CartItems items={cartItems} onUpdateQuantity={updateQuantity} onRemove={removeFromCart} />
{/* 优惠券区域 */}
<div className="coupon-section">
<h3>优惠券</h3>
<div className="coupon-input">
<input
type="text"
value={couponCode}
onChange={e => setCouponCode(e.target.value)}
placeholder="输入优惠券代码"
disabled={!!appliedCoupon}
/>
{appliedCoupon ? (
<button onClick={removeCoupon} className="remove-coupon-btn">
移除
</button>
) : (
<button onClick={applyCoupon} className="apply-coupon-btn">
应用
</button>
)}
</div>
{appliedCoupon && (
<div className="applied-coupon">
<span>已应用: {appliedCoupon.code}</span>
<span className="discount-amount">
-¥{discount.toFixed(2)}
</span>
</div>
)}
<div className="coupon-tips">
<p>可用优惠券:</p>
<ul>
<li>SAVE10 - 9折优惠</li>
<li>SAVE20 - 8折优惠(满¥1000)</li>
<li>SAVE100 - 减¥100(满¥500)</li>
<li>FREESHIP - 包邮</li>
</ul>
</div>
</div>
{/* 价格汇总 */}
<div className="price-summary">
<div className="summary-row">
<span>商品小计</span>
<span>¥{subtotal.toFixed(2)}</span>
</div>
{discount > 0 && (
<div className="summary-row discount">
<span>优惠</span>
<span>-¥{discount.toFixed(2)}</span>
</div>
)}
<div className="summary-row">
<span>运费</span>
<span>
{shipping === 0 ? (
<span className="free-shipping">免运费</span>
) : (
`¥${shipping.toFixed(2)}`
)}
</span>
</div>
{subtotal > 0 && subtotal < 299 && shipping > 0 && (
<div className="shipping-tip">
再购买 ¥{(299 - subtotal).toFixed(2)} 即可包邮
</div>
)}
<div className="summary-row total">
<span>应付总额</span>
<span className="total-price">¥{total.toFixed(2)}</span>
</div>
</div>
<button className="checkout-btn" disabled={cartItems.length === 0}>
去结算 ({cartItems.length})
</button>
</div>
</div>
);
}2.2 添加收藏功能
jsx
function CartWithWishlist() {
const [products] = useState([/* ... */]);
const [cartItems, setCartItems] = useState([]);
const [wishlist, setWishlist] = useState([]);
// 添加到收藏
const addToWishlist = (product) => {
setWishlist(prev => {
if (prev.find(item => item.id === product.id)) {
return prev; // 已存在
}
return [...prev, product];
});
};
// 从收藏移除
const removeFromWishlist = (productId) => {
setWishlist(prev => prev.filter(item => item.id !== productId));
};
// 从收藏移到购物车
const moveToCart = (product) => {
addToCart(product);
removeFromWishlist(product.id);
};
const isInWishlist = (productId) => {
return wishlist.some(item => item.id === productId);
};
return (
<div className="shopping-app">
<nav>
<span>购物车 ({cartItems.length})</span>
<span>收藏 ({wishlist.length})</span>
</nav>
<ProductList
products={products}
onAddToCart={addToCart}
onAddToWishlist={addToWishlist}
isInWishlist={isInWishlist}
/>
<Cart items={cartItems} />
<Wishlist
items={wishlist}
onMoveToCart={moveToCart}
onRemove={removeFromWishlist}
/>
</div>
);
}2.3 添加搜索和筛选
jsx
function CartWithFilter() {
const [products] = useState([/* ... */]);
const [cartItems, setCartItems] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [category, setCategory] = useState('all');
const [priceRange, setPriceRange] = useState({ min: 0, max: Infinity });
const [sortBy, setSortBy] = useState('default');
// 过滤商品
const filteredProducts = useMemo(() => {
return products.filter(product => {
// 搜索过滤
const matchesSearch = product.name
.toLowerCase()
.includes(searchTerm.toLowerCase());
// 分类过滤
const matchesCategory = category === 'all' || product.category === category;
// 价格过滤
const matchesPrice = product.price >= priceRange.min &&
product.price <= priceRange.max;
return matchesSearch && matchesCategory && matchesPrice;
});
}, [products, searchTerm, category, priceRange]);
// 排序商品
const sortedProducts = useMemo(() => {
const sorted = [...filteredProducts];
switch (sortBy) {
case 'price-asc':
return sorted.sort((a, b) => a.price - b.price);
case 'price-desc':
return sorted.sort((a, b) => b.price - a.price);
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
default:
return sorted;
}
}, [filteredProducts, sortBy]);
return (
<div className="shopping-cart">
{/* 搜索和筛选栏 */}
<div className="filter-bar">
<input
type="text"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="搜索商品..."
className="search-input"
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">全部分类</option>
<option value="electronics">电子产品</option>
<option value="accessories">配件</option>
<option value="wearables">可穿戴</option>
</select>
<div className="price-filter">
<input
type="number"
value={priceRange.min}
onChange={e => setPriceRange(prev => ({ ...prev, min: Number(e.target.value) }))}
placeholder="最低价"
/>
<span>-</span>
<input
type="number"
value={priceRange.max === Infinity ? '' : priceRange.max}
onChange={e => setPriceRange(prev => ({
...prev,
max: e.target.value ? Number(e.target.value) : Infinity
}))}
placeholder="最高价"
/>
</div>
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="default">默认排序</option>
<option value="price-asc">价格升序</option>
<option value="price-desc">价格降序</option>
<option value="name">名称排序</option>
</select>
</div>
<p className="result-count">
找到 {sortedProducts.length} 个商品
</p>
<div className="product-grid">
{sortedProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={addToCart}
/>
))}
</div>
<Cart items={cartItems} />
</div>
);
}第三部分:高级功能
3.1 本地存储持久化
jsx
// 自定义Hook: useLocalStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('读取localStorage失败:', error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('写入localStorage失败:', error);
}
};
return [storedValue, setValue];
}
// 使用持久化
function PersistentCart() {
const [products] = useState([/* ... */]);
const [cartItems, setCartItems] = useLocalStorage('shopping-cart', []);
const [wishlist, setWishlist] = useLocalStorage('wishlist', []);
// 添加同步提示
const [lastSaved, setLastSaved] = useState(null);
useEffect(() => {
setLastSaved(new Date());
}, [cartItems, wishlist]);
return (
<div>
{lastSaved && (
<div className="save-indicator">
最后保存: {lastSaved.toLocaleTimeString()}
</div>
)}
<ProductList products={products} onAddToCart={addToCart} />
<Cart items={cartItems} />
<Wishlist items={wishlist} />
</div>
);
}3.2 库存管理
jsx
function CartWithStockManagement() {
const [products, setProducts] = useState([
{ id: 1, name: 'Product 1', price: 100, stock: 10 },
{ id: 2, name: 'Product 2', price: 200, stock: 5 },
{ id: 3, name: 'Product 3', price: 300, stock: 0 }
]);
const [cartItems, setCartItems] = useState([]);
// 检查库存
const checkStock = (productId, quantity) => {
const product = products.find(p => p.id === productId);
return product && product.stock >= quantity;
};
// 添加到购物车(检查库存)
const addToCart = (product) => {
setCartItems(prev => {
const existing = prev.find(item => item.id === product.id);
const newQuantity = existing ? existing.quantity + 1 : 1;
// 检查库存
if (!checkStock(product.id, newQuantity)) {
alert('库存不足');
return prev;
}
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};
// 更新数量(检查库存)
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId);
return;
}
if (!checkStock(productId, quantity)) {
alert('库存不足');
return;
}
setCartItems(prev =>
prev.map(item =>
item.id === productId ? { ...item, quantity } : item
)
);
};
// 获取可用库存
const getAvailableStock = (productId) => {
const product = products.find(p => p.id === productId);
const cartItem = cartItems.find(item => item.id === productId);
if (!product) return 0;
return product.stock - (cartItem ? cartItem.quantity : 0);
};
return (
<div>
{/* 商品列表 */}
{products.map(product => {
const availableStock = getAvailableStock(product.id);
const inCart = cartItems.find(item => item.id === product.id);
return (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>¥{product.price}</p>
<p>库存: {availableStock}</p>
{inCart && <p className="in-cart">购物车中: {inCart.quantity}</p>}
<button
onClick={() => addToCart(product)}
disabled={availableStock === 0}
>
{availableStock === 0 ? '缺货' : '加入购物车'}
</button>
</div>
);
})}
{/* 购物车 */}
<Cart
items={cartItems}
onUpdateQuantity={updateQuantity}
getAvailableStock={getAvailableStock}
/>
</div>
);
}3.3 结算流程
jsx
function CheckoutProcess() {
const [cartItems, setCartItems] = useState([]);
const [step, setStep] = useState(1); // 1:购物车 2:地址 3:支付 4:完成
const [shippingInfo, setShippingInfo] = useState({
name: '',
phone: '',
address: '',
city: '',
zipCode: ''
});
const [paymentMethod, setPaymentMethod] = useState('alipay');
const total = useMemo(() => {
return cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
}, [cartItems]);
// 验证配送信息
const validateShipping = () => {
const { name, phone, address, city, zipCode } = shippingInfo;
if (!name || !phone || !address || !city || !zipCode) {
alert('请填写完整的配送信息');
return false;
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
alert('请输入正确的手机号');
return false;
}
return true;
};
// 下一步
const nextStep = () => {
if (step === 1 && cartItems.length === 0) {
alert('购物车是空的');
return;
}
if (step === 2 && !validateShipping()) {
return;
}
setStep(s => s + 1);
};
// 上一步
const prevStep = () => {
setStep(s => Math.max(1, s - 1));
};
// 提交订单
const submitOrder = async () => {
try {
const order = {
items: cartItems,
shippingInfo,
paymentMethod,
total
};
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('订单已提交:', order);
// 清空购物车
setCartItems([]);
setStep(4);
} catch (error) {
alert('订单提交失败,请重试');
}
};
return (
<div className="checkout-process">
{/* 步骤指示器 */}
<div className="steps">
<div className={`step ${step >= 1 ? 'active' : ''}`}>
1. 购物车
</div>
<div className={`step ${step >= 2 ? 'active' : ''}`}>
2. 配送信息
</div>
<div className={`step ${step >= 3 ? 'active' : ''}`}>
3. 支付方式
</div>
<div className={`step ${step >= 4 ? 'active' : ''}`}>
4. 完成
</div>
</div>
{/* 步骤内容 */}
<div className="step-content">
{step === 1 && (
<CartStep items={cartItems} onUpdateItems={setCartItems} />
)}
{step === 2 && (
<ShippingStep
shippingInfo={shippingInfo}
onUpdate={setShippingInfo}
/>
)}
{step === 3 && (
<PaymentStep
paymentMethod={paymentMethod}
onSelect={setPaymentMethod}
total={total}
/>
)}
{step === 4 && (
<CompletionStep />
)}
</div>
{/* 导航按钮 */}
<div className="navigation">
{step > 1 && step < 4 && (
<button onClick={prevStep} className="prev-btn">
上一步
</button>
)}
{step < 3 && (
<button onClick={nextStep} className="next-btn">
下一步
</button>
)}
{step === 3 && (
<button onClick={submitOrder} className="submit-btn">
提交订单
</button>
)}
</div>
</div>
);
}
// 配送信息步骤
function ShippingStep({ shippingInfo, onUpdate }) {
const handleChange = (field) => (e) => {
onUpdate(prev => ({
...prev,
[field]: e.target.value
}));
};
return (
<div className="shipping-form">
<h2>配送信息</h2>
<div className="form-group">
<label>收货人</label>
<input
value={shippingInfo.name}
onChange={handleChange('name')}
placeholder="请输入收货人姓名"
/>
</div>
<div className="form-group">
<label>手机号</label>
<input
value={shippingInfo.phone}
onChange={handleChange('phone')}
placeholder="请输入手机号"
/>
</div>
<div className="form-group">
<label>详细地址</label>
<textarea
value={shippingInfo.address}
onChange={handleChange('address')}
placeholder="请输入详细地址"
rows={3}
/>
</div>
<div className="form-row">
<div className="form-group">
<label>城市</label>
<input
value={shippingInfo.city}
onChange={handleChange('city')}
placeholder="城市"
/>
</div>
<div className="form-group">
<label>邮编</label>
<input
value={shippingInfo.zipCode}
onChange={handleChange('zipCode')}
placeholder="邮编"
/>
</div>
</div>
</div>
);
}
// 支付方式步骤
function PaymentStep({ paymentMethod, onSelect, total }) {
const paymentMethods = [
{ id: 'alipay', name: '支付宝', icon: '💰' },
{ id: 'wechat', name: '微信支付', icon: '💚' },
{ id: 'card', name: '银行卡', icon: '💳' }
];
return (
<div className="payment-step">
<h2>选择支付方式</h2>
<div className="payment-methods">
{paymentMethods.map(method => (
<div
key={method.id}
className={`payment-method ${paymentMethod === method.id ? 'selected' : ''}`}
onClick={() => onSelect(method.id)}
>
<span className="icon">{method.icon}</span>
<span className="name">{method.name}</span>
{paymentMethod === method.id && <span className="check">✓</span>}
</div>
))}
</div>
<div className="payment-summary">
<h3>订单金额</h3>
<p className="total">¥{total.toFixed(2)}</p>
</div>
</div>
);
}
// 完成步骤
function CompletionStep() {
return (
<div className="completion-step">
<div className="success-icon">✓</div>
<h2>订单提交成功!</h2>
<p>我们会尽快为您发货</p>
<button className="continue-btn">
继续购物
</button>
</div>
);
}第四部分:组件拆分
4.1 ProductCard组件
jsx
const ProductCard = React.memo(function ProductCard({
product,
onAddToCart,
onAddToWishlist,
isInWishlist
}) {
const [isHovered, setIsHovered] = useState(false);
const handleAddToCart = useCallback(() => {
onAddToCart(product);
}, [product, onAddToCart]);
const handleWishlist = useCallback(() => {
onAddToWishlist(product);
}, [product, onAddToWishlist]);
return (
<div
className="product-card"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="product-image">
<img src={product.image} alt={product.name} />
{isHovered && (
<div className="quick-view">
<button className="wishlist-btn" onClick={handleWishlist}>
{isInWishlist ? '❤️' : '🤍'}
</button>
</div>
)}
{product.stock === 0 && (
<div className="out-of-stock">缺货</div>
)}
{product.discount && (
<div className="discount-badge">
-{product.discount}%
</div>
)}
</div>
<div className="product-info">
<h3 className="product-name">{product.name}</h3>
<p className="product-description">{product.description}</p>
<div className="product-footer">
<div className="price-section">
{product.discount ? (
<>
<span className="original-price">
¥{product.price}
</span>
<span className="sale-price">
¥{(product.price * (1 - product.discount / 100)).toFixed(2)}
</span>
</>
) : (
<span className="price">¥{product.price}</span>
)}
</div>
<button
onClick={handleAddToCart}
disabled={product.stock === 0}
className="add-to-cart-btn"
>
{product.stock === 0 ? '缺货' : '加入购物车'}
</button>
</div>
</div>
</div>
);
});
export default ProductCard;4.2 CartItem组件
jsx
const CartItem = React.memo(function CartItem({
item,
onUpdateQuantity,
onRemove,
getAvailableStock
}) {
const [quantity, setQuantity] = useState(item.quantity);
const [isEditing, setIsEditing] = useState(false);
// 立即更新数量
const handleIncrement = useCallback(() => {
const availableStock = getAvailableStock(item.id);
if (quantity < item.stock && quantity < availableStock + item.quantity) {
const newQty = quantity + 1;
setQuantity(newQty);
onUpdateQuantity(item.id, newQty);
}
}, [item.id, item.stock, quantity, getAvailableStock, onUpdateQuantity]);
const handleDecrement = useCallback(() => {
if (quantity > 1) {
const newQty = quantity - 1;
setQuantity(newQty);
onUpdateQuantity(item.id, newQty);
}
}, [item.id, quantity, onUpdateQuantity]);
// 手动输入数量
const handleInputChange = (e) => {
const value = parseInt(e.target.value) || 1;
setQuantity(value);
};
const handleInputBlur = () => {
const validQuantity = Math.max(1, Math.min(quantity, item.stock));
setQuantity(validQuantity);
onUpdateQuantity(item.id, validQuantity);
setIsEditing(false);
};
const handleRemove = useCallback(() => {
onRemove(item.id);
}, [item.id, onRemove]);
const subtotal = item.price * item.quantity;
return (
<div className="cart-item">
<div className="item-image">
<img src={item.image} alt={item.name} />
</div>
<div className="item-details">
<h4>{item.name}</h4>
<p className="item-specs">{item.specs}</p>
<p className="unit-price">单价: ¥{item.price}</p>
</div>
<div className="quantity-control">
<button
onClick={handleDecrement}
disabled={quantity <= 1}
className="qty-btn"
>
-
</button>
{isEditing ? (
<input
type="number"
value={quantity}
onChange={handleInputChange}
onBlur={handleInputBlur}
onKeyPress={e => e.key === 'Enter' && handleInputBlur()}
autoFocus
className="qty-input-edit"
/>
) : (
<span
className="qty-display"
onClick={() => setIsEditing(true)}
>
{quantity}
</span>
)}
<button
onClick={handleIncrement}
disabled={quantity >= item.stock}
className="qty-btn"
>
+
</button>
</div>
<div className="item-subtotal">
<p className="subtotal-label">小计</p>
<p className="subtotal-price">¥{subtotal.toFixed(2)}</p>
</div>
<button onClick={handleRemove} className="remove-btn">
删除
</button>
</div>
);
});
export default CartItem;4.3 CartSummary组件
jsx
const CartSummary = React.memo(function CartSummary({
subtotal,
discount,
shipping,
total,
onCheckout,
onClearCart
}) {
return (
<div className="cart-summary">
<h3>价格明细</h3>
<div className="summary-details">
<div className="summary-row">
<span>商品金额</span>
<span>¥{subtotal.toFixed(2)}</span>
</div>
{discount > 0 && (
<div className="summary-row discount-row">
<span>优惠金额</span>
<span className="discount-amount">-¥{discount.toFixed(2)}</span>
</div>
)}
<div className="summary-row">
<span>运费</span>
<span>
{shipping === 0 ? (
<span className="free-shipping">包邮</span>
) : (
`¥${shipping.toFixed(2)}`
)}
</span>
</div>
<div className="summary-divider"></div>
<div className="summary-row total-row">
<span>应付总额</span>
<span className="total-amount">¥{total.toFixed(2)}</span>
</div>
</div>
<div className="summary-actions">
<button onClick={onClearCart} className="clear-cart-btn">
清空购物车
</button>
<button onClick={onCheckout} className="checkout-btn">
去结算
</button>
</div>
</div>
);
});
export default CartSummary;第五部分:性能优化
5.1 使用useCallback和memo
jsx
function OptimizedCart() {
const [products] = useState([/* ... */]);
const [cartItems, setCartItems] = useState([]);
// 缓存所有回调函数
const addToCart = useCallback((product) => {
setCartItems(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
}, []);
const updateQuantity = useCallback((id, quantity) => {
setCartItems(prev =>
prev.map(item =>
item.id === id ? { ...item, quantity } : item
)
);
}, []);
const removeItem = useCallback((id) => {
setCartItems(prev => prev.filter(item => item.id !== id));
}, []);
const clearCart = useCallback(() => {
setCartItems([]);
}, []);
// 缓存计算结果
const totals = useMemo(() => {
const subtotal = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const shipping = subtotal >= 299 ? 0 : 20;
const total = subtotal + shipping;
return { subtotal, shipping, total };
}, [cartItems]);
return (
<div>
<MemoProductList products={products} onAddToCart={addToCart} />
<MemoCart
items={cartItems}
onUpdateQuantity={updateQuantity}
onRemove={removeItem}
onClear={clearCart}
totals={totals}
/>
</div>
);
}
const MemoProductList = React.memo(function ProductList({ products, onAddToCart }) {
console.log('ProductList渲染');
return (
<div className="product-grid">
{products.map(product => (
<MemoProductCard
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
);
});
const MemoProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
console.log('ProductCard渲染:', product.id);
const handleClick = useCallback(() => {
onAddToCart(product);
}, [product, onAddToCart]);
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>¥{product.price}</p>
<button onClick={handleClick}>加入购物车</button>
</div>
);
});
const MemoCart = React.memo(function Cart({
items,
onUpdateQuantity,
onRemove,
onClear,
totals
}) {
console.log('Cart渲染');
return (
<div className="cart">
{items.map(item => (
<MemoCartItem
key={item.id}
item={item}
onUpdateQuantity={onUpdateQuantity}
onRemove={onRemove}
/>
))}
<CartSummary
subtotal={totals.subtotal}
shipping={totals.shipping}
total={totals.total}
onClear={onClear}
/>
</div>
);
});
const MemoCartItem = React.memo(function CartItem({ item, onUpdateQuantity, onRemove }) {
console.log('CartItem渲染:', item.id);
const handleUpdate = useCallback((newQuantity) => {
onUpdateQuantity(item.id, newQuantity);
}, [item.id, onUpdateQuantity]);
const handleRemove = useCallback(() => {
onRemove(item.id);
}, [item.id, onRemove]);
return (
<div className="cart-item">
<img src={item.image} alt={item.name} />
<div className="item-info">
<h4>{item.name}</h4>
<p>¥{item.price}</p>
</div>
<div className="quantity">
<button onClick={() => handleUpdate(item.quantity - 1)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => handleUpdate(item.quantity + 1)}>+</button>
</div>
<div className="subtotal">
¥{(item.price * item.quantity).toFixed(2)}
</div>
<button onClick={handleRemove}>删除</button>
</div>
);
});5.2 虚拟滚动优化
jsx
import { FixedSizeList } from 'react-window';
function VirtualizedProductList({ products, onAddToCart }) {
const Row = useCallback(({ index, style }) => {
const product = products[index];
return (
<div style={style}>
<MemoProductCard
product={product}
onAddToCart={onAddToCart}
/>
</div>
);
}, [products, onAddToCart]);
return (
<FixedSizeList
height={800}
itemCount={products.length}
itemSize={250}
width="100%"
>
{Row}
</FixedSizeList>
);
}第六部分:用户体验优化
6.1 加载状态
jsx
function CartWithLoading() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchProducts()
.then(data => {
setProducts(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="loading-state">
<div className="spinner"></div>
<p>加载中...</p>
</div>
);
}
if (error) {
return (
<div className="error-state">
<p>加载失败: {error}</p>
<button onClick={() => window.location.reload()}>
重试
</button>
</div>
);
}
return <ShoppingCart products={products} />;
}6.2 添加动画效果
jsx
import { useTransition, animated } from 'react-spring';
function AnimatedCart() {
const [cartItems, setCartItems] = useState([]);
// 列表动画
const transitions = useTransition(cartItems, {
keys: item => item.id,
from: { opacity: 0, transform: 'translateX(100%)' },
enter: { opacity: 1, transform: 'translateX(0%)' },
leave: { opacity: 0, transform: 'translateX(-100%)' }
});
return (
<div className="cart">
{transitions((style, item) => (
<animated.div style={style}>
<CartItem item={item} />
</animated.div>
))}
</div>
);
}6.3 添加提示信息
jsx
function CartWithToast() {
const [cartItems, setCartItems] = useState([]);
const [toast, setToast] = useState(null);
// 显示提示
const showToast = (message, type = 'success') => {
setToast({ message, type });
setTimeout(() => setToast(null), 3000);
};
// 添加到购物车(带提示)
const addToCart = (product) => {
setCartItems(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
showToast(`${product.name} 数量已增加`);
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
showToast(`${product.name} 已加入购物车`, 'success');
return [...prev, { ...product, quantity: 1 }];
});
};
// 删除商品(带确认)
const removeFromCart = (productId) => {
const item = cartItems.find(i => i.id === productId);
if (confirm(`确定要删除 ${item.name} 吗?`)) {
setCartItems(prev => prev.filter(i => i.id !== productId));
showToast(`${item.name} 已从购物车移除`, 'info');
}
};
return (
<div>
{toast && (
<div className={`toast toast-${toast.type}`}>
{toast.message}
</div>
)}
<ProductList products={products} onAddToCart={addToCart} />
<Cart items={cartItems} onRemove={removeFromCart} />
</div>
);
}第七部分:React 19版本
7.1 使用Server Actions
jsx
// app/actions/cart.js
'use server';
import { revalidatePath } from 'next/cache';
export async function addToCart(userId, productId, quantity = 1) {
await db.cart.upsert({
where: { userId_productId: { userId, productId } },
update: { quantity: { increment: quantity } },
create: { userId, productId, quantity }
});
revalidatePath('/cart');
return { success: true };
}
export async function updateCartItem(userId, productId, quantity) {
if (quantity <= 0) {
await db.cart.delete({
where: { userId_productId: { userId, productId } }
});
} else {
await db.cart.update({
where: { userId_productId: { userId, productId } },
data: { quantity }
});
}
revalidatePath('/cart');
return { success: true };
}
export async function clearCart(userId) {
await db.cart.deleteMany({
where: { userId }
});
revalidatePath('/cart');
return { success: true };
}
// app/components/Cart.jsx
'use client';
import { useOptimistic, useTransition } from 'react';
import { addToCart, updateCartItem, clearCart } from '../actions/cart';
function ServerCart({ userId, initialItems }) {
const [isPending, startTransition] = useTransition();
const [optimisticItems, updateOptimistic] = useOptimistic(
initialItems,
(state, { action, productId, quantity, product }) => {
switch (action) {
case 'add':
const existing = state.find(item => item.id === productId);
if (existing) {
return state.map(item =>
item.id === productId
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...state, { ...product, quantity: 1 }];
case 'update':
if (quantity <= 0) {
return state.filter(item => item.id !== productId);
}
return state.map(item =>
item.id === productId ? { ...item, quantity } : item
);
case 'clear':
return [];
default:
return state;
}
}
);
const handleAddToCart = async (product) => {
startTransition(async () => {
updateOptimistic({ action: 'add', productId: product.id, product });
await addToCart(userId, product.id);
});
};
const handleUpdateQuantity = async (productId, quantity) => {
startTransition(async () => {
updateOptimistic({ action: 'update', productId, quantity });
await updateCartItem(userId, productId, quantity);
});
};
const handleClearCart = async () => {
startTransition(async () => {
updateOptimistic({ action: 'clear' });
await clearCart(userId);
});
};
return (
<div className="cart">
{isPending && <div className="loading-overlay">处理中...</div>}
<CartItems
items={optimisticItems}
onUpdateQuantity={handleUpdateQuantity}
/>
<button onClick={handleClearCart} disabled={isPending}>
清空购物车
</button>
</div>
);
}7.2 使用useActionState
jsx
'use client';
import { useActionState } from 'react';
import { checkout } from '../actions/checkout';
function CheckoutForm({ cartItems }) {
const [state, formAction, isPending] = useActionState(checkout, null);
return (
<form action={formAction}>
<h2>结算</h2>
<div className="order-summary">
{cartItems.map(item => (
<div key={item.id}>
<span>{item.name} x {item.quantity}</span>
<span>¥{(item.price * item.quantity).toFixed(2)}</span>
</div>
))}
</div>
<div className="shipping-form">
<input name="name" placeholder="收货人" required />
<input name="phone" placeholder="手机号" required />
<textarea name="address" placeholder="详细地址" required />
</div>
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '提交订单'}
</button>
{state?.success && (
<div className="success-message">
订单提交成功!订单号: {state.orderId}
</div>
)}
{state?.error && (
<div className="error-message">
{state.error}
</div>
)}
</form>
);
}第八部分:完整样式
css
/* ShoppingCart.css */
.shopping-cart-app {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 2px solid #eee;
}
.cart-badge {
padding: 10px 20px;
background: #007bff;
color: white;
border-radius: 20px;
}
.main-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
margin-top: 30px;
}
/* 商品列表 */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.product-image {
position: relative;
width: 100%;
height: 200px;
overflow: hidden;
border-radius: 4px;
margin-bottom: 15px;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.discount-badge {
position: absolute;
top: 10px;
right: 10px;
background: #e74c3c;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-weight: bold;
}
.out-of-stock {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 4px;
}
.product-name {
font-size: 16px;
margin: 10px 0;
font-weight: 600;
}
.price {
font-size: 24px;
color: #e74c3c;
font-weight: bold;
margin: 10px 0;
}
.add-to-cart-btn {
width: 100%;
padding: 12px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.add-to-cart-btn:hover {
background: #218838;
}
.add-to-cart-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 购物车 */
.cart-section {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
}
.empty-cart {
text-align: center;
padding: 60px 20px;
color: #999;
}
.cart-items {
margin-bottom: 20px;
}
.cart-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: white;
border-radius: 8px;
margin-bottom: 15px;
}
.cart-item img {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.item-info {
flex: 1;
}
.item-info h4 {
margin: 0 0 5px;
font-size: 16px;
}
.item-price {
color: #e74c3c;
font-weight: bold;
}
.quantity-controls {
display: flex;
align-items: center;
gap: 10px;
}
.qty-btn {
width: 30px;
height: 30px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
}
.qty-btn:hover {
background: #f0f0f0;
}
.qty-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.qty-input {
width: 50px;
text-align: center;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
}
.item-subtotal {
text-align: right;
min-width: 100px;
}
.subtotal-price {
font-weight: bold;
font-size: 18px;
color: #e74c3c;
}
.remove-btn {
padding: 8px 16px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* 价格汇总 */
.price-summary {
background: white;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
}
.summary-row.discount-row {
color: #28a745;
}
.summary-row.total-row {
font-size: 20px;
font-weight: bold;
border-top: 2px solid #eee;
margin-top: 10px;
padding-top: 15px;
}
.total-price {
color: #e74c3c;
}
.free-shipping {
color: #28a745;
font-weight: bold;
}
.shipping-tip {
background: #fff3cd;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
text-align: center;
font-size: 14px;
}
/* 操作按钮 */
.cart-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.clear-cart-btn {
flex: 1;
padding: 15px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.checkout-btn {
flex: 2;
padding: 15px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
}
.checkout-btn:hover {
background: #218838;
}
.checkout-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 优惠券 */
.coupon-section {
background: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.coupon-input {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.coupon-input input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.apply-coupon-btn {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.applied-coupon {
display: flex;
justify-content: space-between;
background: #d4edda;
padding: 10px;
border-radius: 4px;
color: #155724;
}
.discount-amount {
font-weight: bold;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.cart-section {
position: static;
}
.product-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}练习题
基础练习
- 实现基础购物车的增删改功能
- 添加商品数量控制
- 计算并显示总价
进阶练习
- 实现优惠券功能
- 添加库存管理
- 实现收藏列表
- 添加搜索和筛选功能
高级练习
- 实现完整的结算流程
- 使用本地存储持久化购物车
- 添加动画效果提升用户体验
- 使用React 19的Server Actions实现服务端购物车
- 性能优化:使用memo系列Hook和虚拟滚动
总结
通过完成购物车应用,你已经:
- 掌握了复杂State的管理策略
- 学会了数组操作的各种技巧
- 理解了数据计算与派生状态
- 掌握了组件拆分和复用
- 学会了性能优化方法
- 提升了用户体验设计能力
- 体验了React 19的新特性
购物车是电商应用的核心,也是React开发的综合实践。恭喜你完成了Part1的所有学习!你已经掌握了React核心基础,可以开始构建实际的生产级应用了!
继续学习Part2,深入掌握React Hooks的强大功能!