You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中使用useContext更新购物车时MenuItem数量意外变更问题

React购物车与菜品组件状态同步问题修复

问题描述

刚学习React,现在开发菜品展示(MenuItems)和购物车(Cart)功能,需求是菜品可添加到购物车,购物车内支持增减商品数量。目前已实现菜品添加到购物车的功能,但在CartItem组件点击+/-按钮时,MenuItem组件内的商品数量也会被意外更新,怀疑是useContext导致,但不确定具体原因。

原代码

App.js

import react, { useState, useEffect } from "react";
import Header from "./components/Header";
import LandingPage from "./components/LandingPage";
import MenuItems from "./components/menuItems";
import Cart from "./components/Cart";
import ItemContext from "./store/item-context";

function App() {

    const [items, setItems] = useState([]);
    const [total, setTotal] = useState(0);
    useEffect(() => {
        setTotal(() => {
            return items.reduce((acc, eachItem) => {
                return eachItem.quantity + acc;
            }, 0)
        })
    }, [items])

    const [cartBool, setCartBool] = useState(false);
    function AddedItem(item) {

        const foundIndex = items.findIndex(eachItem => {
            return eachItem.title === item.title;
        })

        if (foundIndex !== -1) {
            setItems(prev => {
                prev[foundIndex].quantity = item.quantity;
                return [...prev];
            })
        }
        else {
            setItems(prev => {
                return [...prev, item]
            })
        }
    }
    function handleCartClick() {
        setCartBool(true);
    }

    function handleCloseClick() {
        setCartBool(false);
    }
    return (
        <react.Fragment>
            <ItemContext.Provider value={{
                items: items
            }}>
                {cartBool &&
                    <Cart onCloseClick={handleCloseClick} />}
            

            <div className="parent-container">
                <Header cartCount={total} onCartClick={handleCartClick} />
                <LandingPage />
                <MenuItems onAddItem={AddedItem} />
            </div>
            </ItemContext.Provider>
        </react.Fragment>
    );
}

export default App;
import react from "react";
import MenuItem from "./menuItem";
import MenuContent from "./menuContent";

function MenuItems(props) {

    function handleItems(item){
        props.onAddItem(item);
    }
    return (
        <div className="menu">
            {MenuContent.map(eachItem =>{
                return <MenuItem title={eachItem.title} description={eachItem.description} price={eachItem.price} key={eachItem.key} onAdd={handleItems}/>
            })}
        </div>
    );
}

export default MenuItems;
import react , { useState } from "react";

function MenuItem(props) {
    const [item, setItem] = useState({
        title: "",
        quantity: 0,
        price: ""
    });

    function handleClick(){


        setItem(prev =>{
            return {
                title: props.title,
                quantity: prev.quantity + 1,
                price: props.price
            }
        })  
    }

    function handleSubmit(event){
        event.preventDefault();
        props.onAdd(item);
    }
    return (
        <div className="menu-item">
            <div className="menu-content">
                <h3>{props.title}</h3>
                <p>{props.description}</p>
                <h4>{props.price}</h4>
            </div>
            <form onSubmit={handleSubmit} className="add-items">
                <label htmlFor="Amount">Amount</label>
                <input onChange={() => {}} type="number" name="Amount" value={item.quantity}/>
                <button onClick={handleClick}  type="submit" className="btn btn-lg">Add</button>
            </form>
        </div>
    );
}
export default MenuItem;

Cart.js

import react, { useContext } from "react";
import CartItem from "./cartItem";
import ItemContext from "../store/item-context";
function Cart(props) {
const ctx = useContext(ItemContext);
function handleCloseClick(){
    props.onCloseClick();
}
return (
    
    <div className="cart-modal">
    <div className="card">
        {ctx.items.map((eachItem, index) =>{
            return <CartItem title={eachItem.title} price={eachItem.price} quantity={eachItem.quantity} key={index} onAdd={props.onAddItem} onRemove={props.RemoveItem}/>
        })}
        <footer>
            <button className="btn btn-lg" onClick={handleCloseClick}>Close</button>
            <button className="btn btn-lg">Order</button>
        </footer>
        </div>
    </div>

);
}export default Cart;

cartItem.js

import react, { useState } from "react";
function CartItem(props) {

const [item, setItem] = useState({
    title: props.title,
    price: props.price,
    quantity: props.quantity 
})

function handlePlusClick(){
    setItem(prev =>{
        prev.quantity = prev.quantity + 1
        return prev
    })
    props.onAdd(item);
}

function handleMinusClick(){
    var updatedQuantity;
    setItem(prev =>{
            prev.quantity = prev.quantity -1
            updatedQuantity = prev.quantity
            return prev;
       
    })
    if(updatedQuantity > 0){
        props.onAdd(item);
    }
    else{
        props.onRemove(item);
    }     
}
return (
    <div className="cart-item">
        <div className="cart-content">
            <h1>{props.title}</h1>
            <p>{props.price}
            <span> X {props.quantity}</span>
            </p>
            
        </div>
        <div className="button-controls">
            <button onClick={handleMinusClick}>-</button>
            <button onClick={handlePlusClick}>+</button>
        </div>
    </div>
);
}export default CartItem;

问题原因及修复方案

核心问题

  1. 直接修改状态对象:App.js的AddedItem函数直接修改数组内的原对象,导致MenuItem和CartItem的商品对象共享引用,一处修改全同步。
  2. setState异步性导致旧状态传递:CartItem调用setItem后立刻传递旧状态给回调,因为setState是异步的。
  3. Cart组件未传递必要回调:App渲染Cart时未传入onAddItem和onRemoveItem,导致CartItem的回调函数无效。
  4. MenuItem输入框无交互处理:输入框无法手动修改数量,只能通过按钮累加。

修改后的代码

App.js

import React, { useState, useEffect } from "react";
import Header from "./components/Header";
import LandingPage from "./components/LandingPage";
import MenuItems from "./components/menuItems";
import Cart from "./components/Cart";
import ItemContext from "./store/item-context";

function App() {
    const [items, setItems] = useState([]);
    const [total, setTotal] = useState(0);

    useEffect(() => {
        setTotal(items.reduce((acc, eachItem) => acc + eachItem.quantity, 0));
    }, [items]);

    const [cartBool, setCartBool] = useState(false);

    // 通用商品数量更新函数
    const updateItemQuantity = (title, change) => {
        setItems(prevItems => {
            const itemIndex = prevItems.findIndex(item => item.title === title);
            if (itemIndex === -1) return prevItems;

            const updatedItems = [...prevItems];
            const updatedQuantity = updatedItems[itemIndex].quantity + change;
            
            if (updatedQuantity <= 0) {
                return updatedItems.filter((_, idx) => idx !== itemIndex);
            } else {
                updatedItems[itemIndex] = {
                    ...updatedItems[itemIndex],
                    quantity: updatedQuantity
                };
                return updatedItems;
            }
        });
    };

    const addItemToCart = (item) => {
        setItems(prevItems => {
            const existingItem = prevItems.find(i => i.title === item.title);
            if (existingItem) {
                return prevItems.map(i => 
                    i.title === item.title 
                        ? {...i, quantity: i.quantity + item.quantity} 
                        : i
                );
            } else {
                return [...prevItems, {...item}];
            }
        });
    };

    const handleCartClick = () => setCartBool(true);
    const handleCloseClick = () => setCartBool(false);

    return (
        <React.Fragment>
            <ItemContext.Provider value={{ items }}>
                {cartBool && (
                    <Cart 
                        onCloseClick={handleCloseClick} 
                        onAddItem={(title) => updateItemQuantity(title, 1)}
                        onRemoveItem={(title) => updateItemQuantity(title, -1)}
                    />
                )}
                <div className="parent-container">
                    <Header cartCount={total} onCartClick={handleCartClick} />
                    <LandingPage />
                    <MenuItems onAddItem={addItemToCart} />
                </div>
            </ItemContext.Provider>
        </React.Fragment>
    );
}

export default App;

Menu-item.js

import React, { useState } from "react";

function MenuItem(props) {
    const [quantity, setQuantity] = useState(0);

    const handleQuantityChange = (e) => {
        const num = parseInt(e.target.value);
        setQuantity(isNaN(num) ? 0 : Math.max(0, num));
    };

    const handleSubmit = (event) => {
        event.preventDefault();
        if (quantity <= 0) return;
        props.onAdd({
            title: props.title,
            quantity: quantity,
            price: props.price
        });
        setQuantity(0);
    };

    return (
        <div className="menu-item">
            <div className="menu-content">
                <h3>{props.title}</h3>
                <p>{props.description}</p>
                <h4>{props.price}</h4>
            </div>
            <form onSubmit={handleSubmit} className="add-items">
                <label htmlFor="Amount">Amount</label>
                <input 
                    onChange={handleQuantityChange} 
                    type="number" 
                    name="Amount" 
                    value={quantity}
                    min="0"
                />
                <button type="submit" className="btn btn-lg">Add</button>
            </form>
        </div>
    );
}

export default MenuItem;

Cart.js

import React, { useContext } from "react";
import CartItem from "./cartItem";
import ItemContext from "../store/item-context";

function Cart(props) {
    const ctx = useContext(ItemContext);

    return (
        <div className="cart-modal">
            <div className="card">
                {ctx.items.map((eachItem) => (
                    <CartItem 
                        key={eachItem.title}
                        title={eachItem.title} 
                        price={eachItem.price} 
                        quantity={eachItem.quantity} 
                        onAdd={() => props.onAddItem(eachItem.title)}
                        onRemove={() => props.onRemoveItem(eachItem.title)}
                    />
                ))}
                <footer>
                    <button className="btn btn-lg" onClick={props.onCloseClick}>Close</button>
                    <button className="btn btn-lg">Order</button>
                </footer>
            </div>
        </div>
    );
}

export default Cart;

cartItem.js

import React from "react";

function CartItem(props) {
    return (
        <div className="cart-item">
            <div className="cart-content">
                <h1>{props.title}</h1>
                <p>
                    {props.price}
                    <span> X {props.quantity}</span>
                </p>
            </div>
            <div className="button-controls">
                <button onClick={props.onRemove}>-</button>
                <button onClick={props.onAdd}>+</button>
            </div>
        </div>
    );
}

export default CartItem;

修改说明

  1. 状态不可变性:所有状态更新都通过创建新对象/数组实现,彻底解决引用共享问题。
  2. 统一逻辑处理:App内新增updateItemQuantity函数,统一管理商品增减逻辑,避免重复代码。
  3. 移除冗余状态:CartItem不再维护内部状态,直接使用App传递的状态和回调,保持单一数据源。
  4. 优化交互体验:MenuItem输入框添加onChange处理,支持手动输入数量,提交后自动重置。
  5. 补全回调传递:Cart组件接收并传递onAddItem和onRemoveItem给CartItem,确保功能正常。

内容的提问来源于stack exchange,提问作者Bhavya S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 02:39:25