Redux cart对象不可扩展:无法添加itemsPrice属性报错排查
在React+Redux项目开发中,在PlaceOrderScreen.js里尝试给从Redux store获取的cart对象添加itemsPrice、shippingPrice、taxPrice、totalPrice属性时,抛出错误:TypeError: Cannot add property itemsPrice, object is not extensible,但教程中的代码可以正常运行。
错误原因
Redux Toolkit(RTK)默认使用Immer库处理状态更新,它会将store中的状态对象设置为不可扩展(non-extensible),目的是强制开发者通过action修改状态,禁止直接修改原始state对象。直接给cart对象添加属性属于修改Redux的原始state,因此触发了这个错误。
教程中的代码可能未使用Redux Toolkit,或使用了旧版RTK/Redux,没有启用Immer的严格模式,所以可以直接修改对象。
解决方案
方案1:计算后使用独立变量(最简单)
不要直接修改cart对象,而是计算出各个价格后存到独立变量中,在组件中使用这些变量:
修改PlaceOrderScreen.js中的价格计算部分:
const cart = useSelector((store) => store.cart); //Calculate Prices const addDecimals = (num) => { return (Math.round(num * 100) / 100).toFixed(2); }; // 计算后存入独立变量,不修改cart const itemsPrice = addDecimals( cart.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0) ); const shippingPrice = addDecimals(Number(itemsPrice) > 100 ? 0 : 100); const taxPrice = addDecimals(Number((0.15 * itemsPrice).toFixed(2))); const totalPrice = ( Number(itemsPrice) + Number(shippingPrice) + Number(taxPrice) ).toFixed(2);
然后在JSX中将所有cart.itemsPrice替换为itemsPrice,cart.shippingPrice替换为shippingPrice,以此类推。
方案2:在cartSlice中添加派生状态(推荐,符合Redux最佳实践)
将价格计算逻辑放到cart的slice中,作为派生状态,通过选择器获取,避免在组件中处理状态计算:
修改cartSlice.js(路径:./features/addToCart/cartSlice):
import { createSlice } from '@reduxjs/toolkit'; const cartSlice = createSlice({ name: 'cart', initialState: { cartItems: [], shippingAddress: {}, paymentMethod: '', }, reducers: { // 保留你已有的reducer,比如addToCart、removeFromCart、setShippingAddress等 setPaymentMethod: (state, action) => { state.paymentMethod = action.payload; }, }, // 定义派生状态选择器 selectors: { // 计算商品总价 selectItemsPrice: (state) => { const addDecimals = (num) => (Math.round(num * 100) / 100).toFixed(2); return addDecimals(state.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)); }, // 计算运费 selectShippingPrice: (state) => { const addDecimals = (num) => (Math.round(num * 100) / 100).toFixed(2); const itemsTotal = parseFloat(state.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)); return addDecimals(itemsTotal > 100 ? 0 : 100); }, // 计算税费 selectTaxPrice: (state) => { const addDecimals = (num) => (Math.round(num * 100) / 100).toFixed(2); const itemsTotal = parseFloat(state.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)); return addDecimals(Number((0.15 * itemsTotal).toFixed(2))); }, // 计算订单总价 selectTotalPrice: (state) => { const addDecimals = (num) => (Math.round(num * 100) / 100).toFixed(2); const itemsTotal = parseFloat(state.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)); const shipping = itemsTotal > 100 ? 0 : 100; const tax = Number((0.15 * itemsTotal).toFixed(2)); return addDecimals(itemsTotal + shipping + tax); }, }, }); // 导出选择器和action export const { selectItemsPrice, selectShippingPrice, selectTaxPrice, selectTotalPrice } = cartSlice.selectors; export const { /* 你的其他action,比如addToCart等 */ setPaymentMethod } = cartSlice.actions; export default cartSlice.reducer;
然后在PlaceOrderScreen.js中导入并使用这些选择器:
import { selectItemsPrice, selectShippingPrice, selectTaxPrice, selectTotalPrice } from '../features/addToCart/cartSlice'; // ...其他代码 const itemsPrice = useSelector(selectItemsPrice); const shippingPrice = useSelector(selectShippingPrice); const taxPrice = useSelector(selectTaxPrice); const totalPrice = useSelector(selectTotalPrice);
方案3:复制cart对象后添加属性(不推荐)
如果必须在组件中修改对象,可以先复制cart生成新对象,再给新对象添加属性:
const cart = useSelector((store) => store.cart); // 复制cart对象,生成可扩展的副本 const cartWithPrices = { ...cart }; // 给副本添加属性 cartWithPrices.itemsPrice = addDecimals( cart.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0) ); cartWithPrices.shippingPrice = addDecimals(Number(cartWithPrices.itemsPrice) > 100 ? 0 : 100); cartWithPrices.taxPrice = addDecimals(Number((0.15 * cartWithPrices.itemsPrice).toFixed(2))); cartWithPrices.totalPrice = ( Number(cartWithPrices.itemsPrice) + Number(cartWithPrices.shippingPrice) + Number(cartWithPrices.taxPrice) ).toFixed(2);
之后在JSX中使用cartWithPrices代替原始的cart。但这种方法会在每次组件渲染时重复复制和计算,不符合Redux状态管理的规范,仅作为临时解决方案。
内容的提问来源于stack exchange,提问作者Usama BG

