React同商品页加购不同规格时已入车商品属性异常问题
问题复现
React电商项目商品详情页(PDP)加购逻辑异常:用户停留在同一PDP页面操作时,先选择耐克T恤规格为color:black、size:S加购,此时购物车显示1件对应规格商品;不跳转页面,直接重新选择同一款T恤规格为color:white、size:M再次加购,首次加入购物车的商品属性会被同步篡改为第二次选择的规格,最终购物车内出现2件属性完全一致的商品,不符合业务预期。
涉及问题代码如下:
class PDP extends Component { state = { currentProduct: this.props.location.state, selectState: [], testObj: {}, } render() { let product = this.state.currentProduct; let handleChange = (evt,attribute) => { const test = Object.assign({}, product); this.state.testObj[attribute] = evt.target.value; this.state.selectState.push(this.state.testObj); let selectedAttributes = [...new Set(this.state.selectState)]; test.selectedAttributes = selectedAttributes; this.setState((state) => ({ currentProduct: test })) } <button className="addToCart" onClick={() => this.props.data.addProduct(this.state.currentProduct)}>ADD TO CART</button> } addProduct = (product) => { this.setState((state) => ({ cart: state.cart.concat(product), })) } }
问题根因
- 直接修改state引用值,违反React不可变数据原则:
handleChange方法中直接修改this.state.testObj的属性、直接调用push操作this.state.selectState数组,全程没有创建新的对象/数组副本。selectState中存入的全是指向同一个testObj的引用,后续修改testObj属性时,之前存入数组的所有项都会同步变更。 - 浅拷贝未切断内存引用关联:
Object.assign({}, product)仅能实现对象第一层的浅拷贝,对象内部的引用类型属性依然和原对象共用内存地址。加购时直接把state中currentProduct的引用传入addProduct,concat存入购物车数组的也是同一个对象的内存地址,后续页面上修改currentProduct属性时,购物车中已经存储的商品对象会被同步篡改。 - 属性去重逻辑完全无效:
Set结构判断引用类型是否重复时,比对的是内存地址而非对象实际内容,就算selectState里存了多个指向同一个testObj的项,[...new Set(this.state.selectState)]也无法实现属性去重效果。
修复方案
- 移除state中全局复用的
testObj变量,每次选择规格时生成全新的对象副本,所有state修改操作都通过生成新值的方式完成,禁止直接赋值修改原state的引用类型属性 - 加购时对传入的商品对象做深拷贝,彻底切断购物车商品项和当前页面state的引用关联,避免后续页面操作篡改已加购的商品数据
- 移除无效的Set去重逻辑,选中属性直接以键值对形式存储,不需要维护冗余的
selectState数组
修复后的核心参考代码:
class PDP extends Component { state = { currentProduct: this.props.location.state, } // 把事件处理函数从render中移出,避免每次渲染生成新函数 handleChange = (evt, attribute) => { // 生成当前商品的深拷贝副本,不直接修改原state const newProduct = structuredClone(this.state.currentProduct); // 初始化选中属性对象 if (!newProduct.selectedAttributes) newProduct.selectedAttributes = {}; newProduct.selectedAttributes[attribute] = evt.target.value; this.setState({ currentProduct: newProduct }) } addProduct = (product) => { // 加购时做深拷贝,彻底断开和页面state的引用关联 const cartItem = structuredClone(product); this.setState((state) => ({ cart: state.cart.concat(cartItem), })) } render() { const { currentProduct: product } = this.state; return ( <button className="addToCart" onClick={() => this.props.data.addProduct(product)} > ADD TO CART </button> ) } }
提示:上述代码用了原生
structuredClone做深拷贝,兼容现代浏览器环境;如果需要兼容低版本浏览器,或商品对象包含函数、特殊实例等无法被结构化克隆的值,可以替换为对应场景可用的深拷贝实现。
内容的提问来源于stack exchange,提问作者swag777
相关产品推荐
相关产品推荐

