案例 1:电商购物车系统
Zustand 在实际项目中可以高效地管理复杂的状态,比如电商购物车系统。本文将通过一个完整的电商购物车案例,展示如何使用 Zustand 设计和实现购物车的核心功能。
需求分析
电商购物车系统通常需要支持以下功能:
- 商品列表展示
- 添加商品到购物车
- 从购物车移除商品
- 调整商品数量
- 计算商品总价
- 购物车持久化
- 空购物车提示
Store 设计
购物车 Store 实现
javascript
// store/cartStore.js
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
// 定义商品类型
const Product = {
id: '',
name: '',
price: 0,
image: '',
description: '',
}
// 定义购物车商品类型
const CartItem = {
...Product,
quantity: 1,
}
// 创建购物车 Store
export const useCartStore = create(
persist(
immer((set, get) => ({
// 商品列表
products: [
{
id: '1',
name: 'MacBook Pro 16',
price: 1999,
image: 'https://example.com/macbook.jpg',
description: 'Apple MacBook Pro 16英寸笔记本电脑',
},
{
id: '2',
name: 'iPhone 14 Pro',
price: 999,
image: 'https://example.com/iphone.jpg',
description: 'Apple iPhone 14 Pro智能手机',
},
{
id: '3',
name: 'AirPods Pro',
price: 249,
image: 'https://example.com/airpods.jpg',
description: 'Apple AirPods Pro无线耳机',
},
],
// 购物车商品列表
cartItems: [],
// 添加商品到购物车
addToCart: (productId) => {
const product = get().products.find(p => p.id === productId)
if (!product) return
set((state) => {
const existingItem = state.cartItems.find(item => item.id === productId)
if (existingItem) {
// 如果商品已存在,增加数量
existingItem.quantity++
} else {
// 如果商品不存在,添加到购物车
state.cartItems.push({
...product,
quantity: 1,
})
}
})
},
// 从购物车移除商品
removeFromCart: (productId) => {
set((state) => {
state.cartItems = state.cartItems.filter(item => item.id !== productId)
})
},
// 调整商品数量
updateQuantity: (productId, quantity) => {
if (quantity <= 0) {
get().removeFromCart(productId)
return
}
set((state) => {
const item = state.cartItems.find(item => item.id === productId)
if (item) {
item.quantity = quantity
}
})
},
// 清空购物车
clearCart: () => {
set({ cartItems: [] })
},
// 计算购物车总价
getTotalPrice: () => {
return get().cartItems.reduce((total, item) => {
return total + (item.price * item.quantity)
}, 0)
},
// 获取购物车商品数量
getCartCount: () => {
return get().cartItems.reduce((count, item) => {
return count + item.quantity
}, 0)
},
// 检查商品是否在购物车中
isInCart: (productId) => {
return get().cartItems.some(item => item.id === productId)
},
}))),
{
// 持久化配置
name: 'cart-storage',
storage: {
getItem: (name) => JSON.parse(localStorage.getItem(name) || 'null'),
setItem: (name, value) => localStorage.setItem(name, JSON.stringify(value)),
removeItem: (name) => localStorage.removeItem(name),
},
}
)
)组件实现
商品列表组件
javascript
// components/ProductList.jsx
import React from 'react'
import { useCartStore } from '../store/cartStore'
function ProductList() {
const products = useCartStore((state) => state.products)
const addToCart = useCartStore((state) => state.addToCart)
const isInCart = useCartStore((state) => state.isInCart)
return (
<div className="product-list">
<h2>商品列表</h2>
<div className="products-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} className="product-image" />
<h3 className="product-name">{product.name}</h3>
<p className="product-description">{product.description}</p>
<p className="product-price">${product.price.toFixed(2)}</p>
<button
className="add-to-cart-btn"
onClick={() => addToCart(product.id)}
disabled={isInCart(product.id)}
>
{isInCart(product.id) ? '已在购物车' : '添加到购物车'}
</button>
</div>
))}
</div>
</div>
)
}
export default ProductList购物车组件
javascript
// components/Cart.jsx
import React from 'react'
import { useCartStore } from '../store/cartStore'
function Cart() {
const cartItems = useCartStore((state) => state.cartItems)
const removeFromCart = useCartStore((state) => state.removeFromCart)
const updateQuantity = useCartStore((state) => state.updateQuantity)
const getTotalPrice = useCartStore((state) => state.getTotalPrice)
const clearCart = useCartStore((state) => state.clearCart)
const getCartCount = useCartStore((state) => state.getCartCount)
// 处理数量变化
const handleQuantityChange = (productId, e) => {
const quantity = parseInt(e.target.value)
if (!isNaN(quantity)) {
updateQuantity(productId, quantity)
}
}
// 空购物车提示
if (cartItems.length === 0) {
return (
<div className="cart empty-cart">
<h2>购物车</h2>
<p>您的购物车是空的</p>
<button className="shop-btn">去购物</button>
</div>
)
}
return (
<div className="cart">
<div className="cart-header">
<h2>购物车 ({getCartCount()})</h2>
<button className="clear-cart-btn" onClick={clearCart}>
清空购物车
</button>
</div>
<div className="cart-items">
{cartItems.map((item) => (
<div key={item.id} className="cart-item">
<img src={item.image} alt={item.name} className="cart-item-image" />
<div className="cart-item-info">
<h3 className="cart-item-name">{item.name}</h3>
<p className="cart-item-price">${item.price.toFixed(2)}</p>
<div className="cart-item-quantity">
<label htmlFor={`quantity-${item.id}`}>数量:</label>
<input
type="number"
id={`quantity-${item.id}`}
value={item.quantity}
onChange={(e) => handleQuantityChange(item.id, e)}
min="1"
className="quantity-input"
/>
</div>
<p className="cart-item-subtotal">
小计:${(item.price * item.quantity).toFixed(2)}
</p>
</div>
<button
className="remove-item-btn"
onClick={() => removeFromCart(item.id)}
aria-label="移除商品"
>
✕
</button>
</div>
))}
</div>
<div className="cart-total">
<h3>总计:${getTotalPrice().toFixed(2)}</h3>
<button className="checkout-btn">去结算</button>
</div>
</div>
)
}
export default Cart购物车数量指示器组件
javascript
// components/CartIndicator.jsx
import React from 'react'
import { useCartStore } from '../store/cartStore'
function CartIndicator() {
const getCartCount = useCartStore((state) => state.getCartCount)
const cartCount = getCartCount()
return (
<div className="cart-indicator">
<span className="cart-icon">🛒</span>
{cartCount > 0 && (
<span className="cart-count">{cartCount}</span>
)}
</div>
)
}
export default CartIndicator页面整合
主页面组件
javascript
// pages/Home.jsx
import React from 'react'
import ProductList from '../components/ProductList'
import Cart from '../components/Cart'
import CartIndicator from '../components/CartIndicator'
function Home() {
return (
<div className="app">
<header className="header">
<h1>电商购物车系统</h1>
<CartIndicator />
</header>
<main className="main-content">
<ProductList />
<Cart />
</main>
<footer className="footer">
<p>© 2023 电商购物车系统 - 使用 Zustand 构建</p>
</footer>
</div>
)
}
export default Home样式实现
css
/* styles.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
.header h1 {
color: #333;
}
/* 购物车指示器样式 */
.cart-indicator {
position: relative;
font-size: 24px;
cursor: pointer;
}
.cart-count {
position: absolute;
top: -8px;
right: -8px;
background-color: #ff4444;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
}
/* 主内容区样式 */
.main-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
/* 商品列表样式 */
.product-list h2 {
margin-bottom: 20px;
color: #333;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.product-card {
background-color: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
.product-card:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.product-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 10px;
}
.product-name {
font-size: 18px;
margin-bottom: 5px;
color: #333;
}
.product-description {
font-size: 14px;
color: #666;
margin-bottom: 10px;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
}
.product-price {
font-size: 16px;
font-weight: bold;
color: #ff4444;
margin-bottom: 15px;
}
.add-to-cart-btn {
background-color: #4caf50;
color: white;
border: none;
padding: 10px;
border-radius: 4px;
cursor: pointer;
width: 100%;
font-size: 14px;
transition: background-color 0.3s ease;
}
.add-to-cart-btn:hover {
background-color: #45a049;
}
.add-to-cart-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
/* 购物车样式 */
.cart {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
position: sticky;
top: 20px;
}
.cart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
.cart-header h2 {
color: #333;
}
.clear-cart-btn {
background-color: #f44336;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.3s ease;
}
.clear-cart-btn:hover {
background-color: #d32f2f;
}
.cart-items {
margin-bottom: 20px;
max-height: 400px;
overflow-y: auto;
}
.cart-item {
display: flex;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.cart-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.cart-item-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.cart-item-info {
flex: 1;
}
.cart-item-name {
font-size: 14px;
margin-bottom: 5px;
color: #333;
}
.cart-item-price {
font-size: 14px;
color: #ff4444;
margin-bottom: 5px;
}
.cart-item-quantity {
display: flex;
align-items: center;
}
.quantity-input {
width: 50px;
padding: 4px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
}
.remove-item-btn {
background-color: transparent;
color: #ff4444;
border: none;
font-size: 18px;
cursor: pointer;
padding: 5px;
}
.cart-total {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 15px;
border-top: 1px solid #e0e0e0;
}
.cart-total h3 {
font-size: 18px;
color: #333;
}
.checkout-btn {
background-color: #2196f3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.checkout-btn:hover {
background-color: #1976d2;
}
/* 空购物车样式 */
.empty-cart {
text-align: center;
padding: 40px 20px;
}
.empty-cart p {
margin-bottom: 20px;
color: #666;
}
.shop-btn {
background-color: #4caf50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.shop-btn:hover {
background-color: #45a049;
}
/* 页脚样式 */
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #666;
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.cart {
position: static;
}
}功能特性
状态管理:使用 Zustand 管理购物车状态,包括商品列表、购物车商品、数量等
中间件集成:
persist中间件实现购物车持久化immer中间件简化状态更新
性能优化:
- 使用 Selector 模式订阅所需状态
- 避免不必要的组件重渲染
用户体验:
- 商品添加状态反馈
- 购物车数量实时更新
- 空购物车提示
- 响应式设计
总结
通过这个电商购物车系统案例,我们展示了如何使用 Zustand 构建一个完整的购物车功能。Zustand 的简洁 API 和强大的中间件支持使得状态管理变得简单高效,同时也提供了良好的性能和用户体验。
这个案例可以根据实际需求进行扩展,比如添加用户认证、支付功能、订单管理等。