MERN+Redux Toolkit更新产品后出现读取img的undefined错误求助
问题描述
使用MERN栈及旧版Redux Toolkit开发项目时,在产品页面更新选中产品后,后端更新路由无报错,数据库与Redux状态均已成功修改,但页面出现错误:cannot read properties of undefined (reading 'img'),需返回主页多次刷新才能消除该错误。
核心原因
1. 更新后的产品对象缺失_id字段
在更新产品的API调用逻辑中,构造的更新对象未包含产品的_id属性:
// 原代码中构造的更新对象 const product = { ...inputs, img: downloadURL, categories: cat };
Redux的updateProductSuccess reducer直接用这个对象替换原数组中的产品项,导致更新后的产品对象没有_id。当组件重新渲染时,useSelector通过product._id === productId查找产品时会找不到匹配项,返回undefined,此时访问product.img就会触发错误。
2. 组件未处理product为undefined的边界情况
组件初始化时,Redux的products数组可能尚未加载完成;或者更新后因_id缺失找不到产品,此时product为undefined,但模板中直接访问product.img、product.title等属性,未做任何空值保护,导致页面报错。
3. Redux Persist缓存与新状态不一致
由于使用了redux-persist持久化状态,旧的产品数据可能被缓存在本地存储中,更新后的新状态无法立即覆盖缓存,需要多次刷新页面才能让缓存与最新状态同步,这也是多次刷新后错误消失的原因。
解决方案
1. 修复更新对象,保留_id字段
在构造更新的产品对象时,加入_id字段,确保更新后的对象能被正确识别:
// 修改apiCall中的构造逻辑 getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => { const product = { ...inputs, img: downloadURL, categories: cat, _id: productId }; updateProduct(dispatch, product, productId); });
2. 给组件添加空值保护
在模板中使用可选链操作符?.处理可能的undefined情况,或者在组件开头添加加载判断:
// 方式1:使用可选链操作符 <img src={product?.img} alt="img" className="productInfoImg" /> <span className="productName">{product?.title}</span> <input name="title" type="text" placeholder={product?.title} onChange={handleChange} /> // 方式2:添加加载状态判断 if (!product) { return <div>加载中...</div>; }
3. 优化Redux Reducer的更新逻辑
在updateProductSuccess中先检查索引是否有效,避免给数组的无效位置赋值;同时修正原reducer中isFetching状态的错误设置:
updateProductSuccess: (state, action) => { state.isFetching = false; const index = state.products.findIndex((item) => item._id === action.payload.id); // 确保找到对应产品后再更新 if (index !== -1) { state.products[index] = action.payload.product; } }, // 同时修正所有Failure类型reducer的isFetching状态,原代码错误设置为true,应改为false getProductFailure: (state) => { state.isFetching = false; state.error = true; }, deleteProductFailure: (state) => { state.isFetching = false; state.error = true; }, updateProductFailure: (state) => { state.isFetching = false; state.error = true; }, addProductFailure: (state) => { state.isFetching = false; state.error = true; },
4. 同步Redux Persist状态
在组件初始化时,主动触发产品列表的获取,确保缓存状态与服务器最新数据同步:
useEffect(() => { dispatch(getProductStart()); // 调用获取所有产品的API逻辑,示例代码 fetch('/api/products') .then(res => res.json()) .then(data => dispatch(getProductSuccess(data))) .catch(() => dispatch(getProductFailure())); }, [dispatch]);
相关代码参考
完整ProductRedux代码(已修正)
import { createSlice } from "@reduxjs/toolkit"; export const productSlice = createSlice({ name: "product", initialState: { products: [], isFetching: false, error: false, }, reducers: { // GET ALL getProductStart: (state) => { state.isFetching = true; state.error = false; }, getProductSuccess: (state, action) => { state.isFetching = false; state.products = action.payload; }, getProductFailure: (state) => { state.isFetching = false; state.error = true; }, // DELETE deleteProductStart: (state) => { state.isFetching = true; state.error = false; }, deleteProductSuccess: (state, action) => { state.isFetching = false; state.products.splice( state.products.findIndex((item) => item._id === action.payload),1 ) }, deleteProductFailure: (state) => { state.isFetching = false; state.error = true; }, // UPDATE updateProductStart: (state) => { state.isFetching = true; state.error = false; }, updateProductSuccess: (state, action) => { state.isFetching = false; const index = state.products.findIndex((item) => item._id === action.payload.id); if (index !== -1) { state.products[index] = action.payload.product; } }, updateProductFailure: (state) => { state.isFetching = false; state.error = true; }, // ADD addProductStart: (state) => { state.isFetching = true; state.error = false; }, addProductSuccess: (state, action) => { state.isFetching = false; state.products.push(action.payload); }, addProductFailure: (state) => { state.isFetching = false; state.error = true; }, }, }); export const { getProductStart, getProductSuccess, getProductFailure, deleteProductStart, deleteProductSuccess, deleteProductFailure, updateProductStart, updateProductSuccess, updateProductFailure, addProductStart, addProductSuccess, addProductFailure } = productSlice.actions; export default productSlice.reducer;
产品页面关键修改代码
export default function Product() { const location = useLocation(); const productId = location.pathname.split("/")[2]; const [ productStats, setProductStats ] = useState([]); const [ inputs, setInputs ] = useState({}); const [ file, setFile ] = useState(null); const [ cat, setCat ] = useState([]); const dispatch = useDispatch(); const product = useSelector((state) => state.product.products.find((product) => product._id === productId) ); // 添加加载判断 if (!product) { return <div>加载中...</div>; } const handleChange = (e) => { setInputs((prev) => { return { ...prev, [e.target.name]: e.target.value } }) } const handleCat = (e) => { setCat(e.target.value.split(",")); } useEffect(() => { if (cat.length === 0) { setCat(product.categories); } }, [product.categories, cat.length]) // 初始化时获取最新产品列表 useEffect(() => { dispatch(getProductStart()); fetch('/api/products') .then(res => res.json()) .then(data => dispatch(getProductSuccess(data))) .catch(() => dispatch(getProductFailure())); }, [dispatch]) // ... 其余代码保持不变 }
内容的提问来源于stack exchange,提问作者RAHMAN DHIKA

