为了账号安全,请及时绑定邮箱和手机立即绑定

使用react实现商城购物车功能

标签:
React

在电商类前端项目中,购物车是核心功能之一:用户需要在不离开列表或详情页的情况下将商品加入购物车,并在统一页面中查看、修改数量或删除。使用 React 实现购物车时,需要解决“状态在哪里存”、“谁可以读写”以及“如何与页面联动”三个问题。本文通过一个简单的React 商城示例来说明如何用 “Context API”管理购物车状态,并完成列表页、详情页的「加入购物车」与购物车列表页的展示与操作。

项目结构

https://img1.sycdn.imooc.com/229bb56909afd53617401089.jpg

首先我们应该考虑的问题就是状态放在哪里?购物车数据会被多个页面使用:商品列表、商品详情、购物车页、顶栏角标等。如果通过层层 props 传递,会非常繁琐且容易出错。因此我们采用 **React Context** 在应用顶层提供「购物车上下文」,任何子组件只要调用 `useCart()` 就能读写购物车,无需逐层传参。

/**
 * 根组件
 * - 用 CartProvider 包裹整站,使任意子组件都能通过 useCart 访问购物车
 * - Layout 提供顶栏、主内容区、页脚;主内容区根据路由渲染不同页面
 * - 路由:/ 商品列表,/product/:id 商品详情,/cart 购物车
 */
import { Routes, Route } from 'react-router-dom'
import { CartProvider } from './context/CartContext'
import Layout from './components/Layout'
import ProductList from './pages/ProductList'
import ProductDetail from './pages/ProductDetail'
import Cart from './pages/Cart'

function App() {
  return (
    <CartProvider>
      <Layout>
        <Routes>
          <Route path="/" element={<ProductList />} />
          <Route path="/product/:id" element={<ProductDetail />} />
          <Route path="/cart" element={<Cart />} />
        </Routes>
      </Layout>
    </CartProvider>
  )
}

export default App

购物车我们可以只存「商品 ID + 数量」的列表,例如:

```js
[
  { productId: 1, quantity: 2 },
  { productId: 3, quantity: 1 }
]
```
商品详情(名称、价格、图片等)仍然从原有的商品数据源(如 `products.js`)里根据 ID 查询。这样避免重复存储、便于和后台接口对齐(通常后台也只存 id 与数量)。

我们要创建 Context 与 Provider,可以在一个单独的文件(如 `CartContext.jsx`)中完成以下事情:
1. 使用 `createContext` 创建一个空的购物车上下文。
2. 在组件内用 `useState` 维护 `items` 数组(即 `[{ productId, quantity }, ...]`)。
3. 实现 `addToCart(productId, quantity)`:先校验商品是否存在,再在 `items` 中查找是否已有该 `productId`;若有则把对应项的 `quantity` 加上传入的数量,若无则追加新项。
4. 实现 `updateQuantity(productId, quantity)`:遍历 `items`,将对应 `productId` 的 `quantity` 改为新值(可约定数量小于 1 时不处理或视为删除)。
5. 实现 `removeFromCart(productId)`:从 `items` 中过滤掉该 `productId` 的项。
6. 用 `items` 计算总件数 `cartCount`(所有 `quantity` 之和)。
7. 将 `items`、`addToCart`、`updateQuantity`、`removeFromCart`、`cartCount` 通过 `CartContext.Provider` 的 `value` 传给子树。被 `CartProvider` 包裹的任意组件都可以通过 `useContext(CartContext)` 或自定义 Hook `useCart()` 访问上述状态和方法。

CartContext.jsx组件

/**
 * 购物车上下文
 * - 用 Context 在整站共享购物车状态,避免逐层传 props
 * - 购物车数据格式:items = [{ productId, quantity }, ...],只存 ID 和数量,商品详情从 products 按 ID 查
 */
import { createContext, useContext, useState, useCallback } from 'react'
import { getProductById } from '../data/products'

const CartContext = createContext(null)

/** 购物车状态提供者,包裹在 App 根组件外层 */
export function CartProvider({ children }) {
  const [items, setItems] = useState([]) // [{ productId, quantity }, ...]

  /** 加入购物车:若已存在则数量累加,否则新增一项;会校验商品是否存在 */
  const addToCart = useCallback((productId, quantity = 1) => {
    const id = Number(productId)
    if (!getProductById(id)) return
    setItems((prev) => {
      const existing = prev.find((i) => i.productId === id)
      if (existing) {
        return prev.map((i) =>
          i.productId === id ? { ...i, quantity: i.quantity + quantity } : i
        )
      }
      return [...prev, { productId: id, quantity }]
    })
  }, [])

  /** 修改某商品数量,小于 1 不处理 */
  const updateQuantity = useCallback((productId, quantity) => {
    if (quantity < 1) return
    setItems((prev) =>
      prev.map((i) =>
        i.productId === Number(productId) ? { ...i, quantity } : i
      )
    )
  }, [])

  /** 从购物车移除指定商品 */
  const removeFromCart = useCallback((productId) => {
    setItems((prev) => prev.filter((i) => i.productId !== Number(productId)))
  }, [])

  /** 购物车总件数(所有 quantity 之和),用于顶栏角标 */
  const cartCount = items.reduce((sum, i) => sum + i.quantity, 0)

  const value = {
    items,
    addToCart,
    updateQuantity,
    removeFromCart,
    cartCount,
  }

  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  )
}

/** 在任意子组件中调用,获取购物车状态与操作方法;必须在 CartProvider 内部使用 */
export function useCart() {
  const ctx = useContext(CartContext)
  if (!ctx) throw new Error('useCart must be used within CartProvider')
  return ctx
}

各页面之间可以通过一下方式互相配合

商品列表页

- 列表数据仍从商品数据源(如 `products` 数组)读取,不做持久化。
- 每个商品卡片上有一个「加入购物车」按钮,点击时调用 `addToCart(product.id)`。
- 若希望点击卡片图片或标题跳转详情、而点击按钮只加购不跳转,需要把卡片拆成:图片/标题用 `Link` 包起来,按钮单独写,避免整张卡片是一个链接导致按钮无效。

/**
 * 商品列表页(路由:/)
 * - 使用 products 数组渲染商品卡片网格
 * - 点击图片或商品名:跳转详情页;点击「加入购物车」:调用 addToCart,不跳转
 */
import { Link } from 'react-router-dom'
import { products } from '../data/products'
import { useCart } from '../context/CartContext'
import './ProductList.css'

export default function ProductList() {
  const { addToCart } = useCart()
  return (
    <div className="product-list-page">
      <div className="container">
        <h1 className="page-title">商品列表</h1>
        <div className="product-grid">
          {products.map((product) => (
            <article key={product.id} className="product-card">
              {/* 图片区域:点击进入详情 */}
              <Link to={`/product/${product.id}`} className="product-card-image-wrap">
                <img
                  src={product.image}
                  alt={product.name}
                  className="product-card-image"
                />
                <span className="product-card-category">{product.category}</span>
              </Link>
              <div className="product-card-body">
                <Link to={`/product/${product.id}`} className="product-card-name">
                  {product.name}
                </Link>
                <div className="product-card-meta">
                  <span className="product-card-rating">★ {product.rating}</span>
                  <span className="product-card-sales">已售 {product.sales}</span>
                </div>
                <div className="product-card-prices">
                  <span className="product-card-price">¥{product.price}</span>
                  {product.originalPrice > product.price && (
                    <span className="product-card-original">
                      ¥{product.originalPrice}
                    </span>
                  )}
                </div>
                <button
                  type="button"
                  className="product-card-add"
                  onClick={() => addToCart(product.id)}
                >
                  加入购物车
                </button>
              </div>
            </article>
          ))}
        </div>
      </div>
    </div>
  )
}

商品详情页

- 通过路由参数(如 `useParams()`)拿到当前商品 ID,再用 `getProductById(id)` 取详情并渲染。
- 「加入购物车」按钮点击时执行 `addToCart(product.id)`。
- 「立即购买」可先做占位,后续再对接结算或下单流程。

/**
 * 商品详情页(路由:/product/:id)
 * - 从 useParams 取 URL 中的 id,用 getProductById 查商品;找不到则显示「未找到」和返回链接
 * - 「加入购物车」调用 addToCart;「立即购买」为占位,未实现
 */
import { useParams, Link } from 'react-router-dom'
import { getProductById } from '../data/products'
import { useCart } from '../context/CartContext'
import './ProductDetail.css'

export default function ProductDetail() {
  const { id } = useParams()
  const { addToCart } = useCart()
  const product = getProductById(id)

  if (!product) {
    return (
      <div className="product-detail-page">
        <div className="container">
          <p className="detail-not-found">未找到该商品</p>
          <Link to="/" className="detail-back">返回商品列表</Link>
        </div>
      </div>
    )
  }

  return (
    <div className="product-detail-page">
      <div className="container">
        <Link to="/" className="detail-back-link">← 返回列表</Link>

        <article className="detail-layout">
          <div className="detail-gallery">
            <img
              src={product.image}
              alt={product.name}
              className="detail-image"
            />
          </div>

          <div className="detail-info">
            <span className="detail-category">{product.category}</span>
            <h1 className="detail-title">{product.name}</h1>
            <div className="detail-meta">
              <span className="detail-rating">★ {product.rating}</span>
              <span className="detail-sales">已售 {product.sales}</span>
            </div>
            <div className="detail-prices">
              <span className="detail-price">¥{product.price}</span>
              {product.originalPrice > product.price && (
                <span className="detail-original">¥{product.originalPrice}</span>
              )}
            </div>
            <p className="detail-desc">
              品质保证,支持退换。下单即发,通常 1–3 日送达。
            </p>
            <div className="detail-actions">
              <button
                type="button"
                className="btn btn-primary"
                onClick={() => addToCart(product.id)}
              >
                加入购物车
              </button>
              <button type="button" className="btn btn-secondary">
                立即购买
              </button>
            </div>
          </div>
        </article>
      </div>
    </div>
  )
}

购物车列表页
- 从 `useCart()` 取出 `items`。
- 遍历 `items`,对每一项用 `getProductById(productId)` 得到商品信息,与 `quantity` 组合成「一行」数据(注意过滤掉已下架或无效 ID)。
- 用「单价 × 数量」计算每行小计,再汇总得到总价。
- 每行提供「数量 − / +」按钮和「删除」按钮,分别调用 `updateQuantity` 和 `removeFromCart`。
- 当 `items.length === 0` 时展示空状态(如「购物车是空的」+ 跳转列表页的链接)。

/**
 * 购物车页(路由:/cart)
 * - 从 useCart 取 items,用 getProductById 拼成「商品 + 数量」列表 rows,并算总价 total
 * - 空车时显示空状态和「去逛逛」;有数据时展示列表、数量加减、删除、右侧汇总与「去结算」(占位)
 */
import { Link } from 'react-router-dom'
import { useCart } from '../context/CartContext'
import { getProductById } from '../data/products'
import './Cart.css'

export default function Cart() {
  const { items, updateQuantity, removeFromCart } = useCart()

  // 将 items(仅含 productId、quantity)转为带完整商品信息的 rows,无效 id 过滤掉
  const rows = items
    .map(({ productId, quantity }) => ({
      product: getProductById(productId),
      quantity,
    }))
    .filter((r) => r.product)

  const total = rows.reduce((sum, { product, quantity }) => sum + product.price * quantity, 0)

  if (rows.length === 0) {
    return (
      <div className="cart-page">
        <div className="container">
          <h1 className="cart-title">购物车</h1>
          <div className="cart-empty">
            <p>购物车是空的</p>
            <Link to="/" className="btn btn-primary">去逛逛</Link>
          </div>
        </div>
      </div>
    )
  }

  return (
    <div className="cart-page">
      <div className="container">
        <h1 className="cart-title">购物车</h1>
        <div className="cart-layout">
          <ul className="cart-list">
            {rows.map(({ product, quantity }) => (
              <li key={product.id} className="cart-item">
                <Link to={`/product/${product.id}`} className="cart-item-image-wrap">
                  <img src={product.image} alt={product.name} />
                </Link>
                <div className="cart-item-info">
                  <Link to={`/product/${product.id}`} className="cart-item-name">
                    {product.name}
                  </Link>
                  <p className="cart-item-price">¥{product.price}</p>
                  <div className="cart-item-actions">
                    <div className="quantity-wrap">
                      <button
                        type="button"
                        className="qty-btn"
                        onClick={() => updateQuantity(product.id, quantity - 1)}
                      >
                        −
                      </button>
                      <span className="qty-value">{quantity}</span>
                      <button
                        type="button"
                        className="qty-btn"
                        onClick={() => updateQuantity(product.id, quantity + 1)}
                      >
                        +
                      </button>
                    </div>
                    <button
                      type="button"
                      className="cart-remove"
                      onClick={() => removeFromCart(product.id)}
                    >
                      删除
                    </button>
                  </div>
                </div>
                <div className="cart-item-subtotal">
                  ¥{product.price * quantity}
                </div>
              </li>
            ))}
          </ul>
          <aside className="cart-summary">
            <p className="cart-summary-row">
              <span>共 {rows.length} 件</span>
              <span>¥{total}</span>
            </p>
            <button type="button" className="btn btn-primary btn-block">
              去结算
            </button>
          </aside>
        </div>
      </div>
    </div>
  )
}

文章小结:用 React 实现购物车功能时,核心是:“用 Context 在顶层维护「商品 ID + 数量」的列表,对外提供加入、修改数量、删除和总件数”;各页面按需调用这些方法并配合现有商品数据源展示。这样既能避免 props 层层传递,又便于后续扩展(例如将 `items` 持久化到 localStorage 或与后端接口同步)。

本文所述思路可直接对应到示例项目中的 `CartContext.jsx`、商品列表页、商品详情页、购物车页和布局组件,便于对照代码理解实现细节。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
76
获赞与收藏
538

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消