教程 zustand tutorial

案例 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;
  }
}

功能特性

  1. 状态管理:使用 Zustand 管理购物车状态,包括商品列表、购物车商品、数量等

  2. 中间件集成:

    • persist 中间件实现购物车持久化
    • immer 中间件简化状态更新
  3. 性能优化:

    • 使用 Selector 模式订阅所需状态
    • 避免不必要的组件重渲染
  4. 用户体验:

    • 商品添加状态反馈
    • 购物车数量实时更新
    • 空购物车提示
    • 响应式设计

总结

通过这个电商购物车系统案例,我们展示了如何使用 Zustand 构建一个完整的购物车功能。Zustand 的简洁 API 和强大的中间件支持使得状态管理变得简单高效,同时也提供了良好的性能和用户体验。

这个案例可以根据实际需求进行扩展,比如添加用户认证、支付功能、订单管理等。