更新产品时出现TypeError: Cannot read properties of undefined (reading 'img')错误
我在更新产品时,执行操作后出现如下错误:
TypeError: Cannot read properties of undefined (reading 'img')
但产品实际已更新成功,离开当前页面再返回(不刷新)能看到带正确图片的产品。
问题原因
错误直接指向product.img的访问,核心是组件渲染时product对象为undefined,触发场景分两种:
- 组件初始化阶段:Redux中的
products列表还未完成加载,find方法找不到对应产品返回undefined,直接访问属性就报错。 - 产品更新后:Redux状态更新过程中可能出现短暂的状态不一致,导致
product临时变为undefined,引发渲染错误。
解决方案
1. 全局添加空值保护(最快解决)
在所有访问product属性的位置,使用可选链操作符?.,避免product为undefined时报错:
// 产品信息区 <img src={product?.img} alt="" className="productInfoImg" /> <span className="productName">{product?.title}</span> <span className="productInfoValue">{product?._id}</span> <span className="productInfoValue">{product?.inStock}</span> // 表单输入框 <input type="text" placeholder={product?.title} name="title" onChange={handleChange} /> <input type="text" placeholder={product?.desc} name="desc" onChange={handleChange} /> <input type="text" placeholder={product?.price} name="price" onChange={handleChange} /> // 上传预览图 <img src={product?.img} alt="" className="productUploadImg" />
2. 初始化时兜底加载产品数据
如果Redux中没有预加载完整的产品列表,组件初始化时如果找不到product,直接从后端单独获取该产品数据,确保渲染时有数据可用:
// 新增本地state暂存产品数据 const [localProduct, setLocalProduct] = useState(null); useEffect(() => { const fetchProduct = async () => { try { const res = await userRequest.get(`products/${productId}`); setLocalProduct(res.data); } catch (err) { console.error(err); } }; // Redux中找不到产品时,发起请求 if (!product) fetchProduct(); }, [productId, product]); // 渲染时优先用localProduct, fallback到Redux中的product const targetProduct = localProduct || product;
之后把模板中所有的product替换为targetProduct即可。
3. 修复Redux更新逻辑的潜在问题
检查updateProductSuccess中的findIndex是否能正确匹配产品ID,避免因类型不匹配(比如ID是字符串/数字不一致)导致更新失败,进而让product找不到:
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; } },
原代码参考
Product组件
import { Link, useLocation } from "react-router-dom"; import "./product.css"; import Chart from "../../components/chart/Chart"; import { productData } from "../../dummyData"; import { Publish } from "@material-ui/icons"; import { useSelector } from "react-redux"; import { useEffect, useMemo, useState } from "react"; import { userRequest } from "../../requestMethods"; import { updateProduct } from "../../redux/apiCalls"; import { useDispatch } from "react-redux"; import { getStorage, ref, uploadBytesResumable, getDownloadURL, } from "firebase/storage"; import app from "../../firebase"; export default function Product() { const location = useLocation(); const productId = location.pathname.split("/")[2]; const [pStats, setPStats] = useState([]); const product = useSelector((state) => state.product.products.find((product) => product._id === productId) ); const dispatch=useDispatch(); const [inputs, setInputs]=useState({}) const [file, setFile] = useState(null); const [cat, setCat] = useState([]); const handleChange = (e) => { setInputs((prev) => { return { ...prev, [e.target.name]: e.target.value }; }); }; const handleCat = (e) => { setCat(e.target.value.split(",")); }; const handleClick = (e) => { e.preventDefault(); const fileName = new Date().getTime() + file.name; const storage = getStorage(app); const storageRef = ref(storage, fileName); const uploadTask = uploadBytesResumable(storageRef, file); uploadTask.on( "state_changed", (snapshot) => { const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; console.log("Upload is " + progress + "% done"); switch (snapshot.state) { case "paused": console.log("Upload is paused"); break; case "running": console.log("Upload is running"); break; default: } }, (error) => { // Handle unsuccessful uploads }, () => { getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => { const product = { ...inputs, img: downloadURL, categories: cat }; updateProduct(productId, product, dispatch); }); } ); }; const MONTHS = useMemo( () => [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Agu", "Sep", "Oct", "Nov", "Dec", ], [] ); useEffect(() => { const getStats = async () => { try { const res = await userRequest.get("orders/income?pid=" + productId); const list = res.data.sort((a,b)=>{ return a._id - b._id }) list.map((item) => setPStats((prev) => [ ...prev, { name: MONTHS[item._id - 1], Sales: item.total }, ]) ); } catch (err) { console.log(err); } }; getStats(); }, [productId, MONTHS]); return ( <div className="product"> <div className="productTitleContainer"> <h1 className="productTitle">Product</h1> <Link to="/newproduct"> <button className="productAddButton">Create</button> </Link> </div> <div className="productTop"> <div className="productTopLeft"> <Chart data={pStats} dataKey="Sales" title="Sales Performance" /> </div> <div className="productTopRight"> <div className="productInfoTop"> <img src={product.img} alt="" className="productInfoImg" /> <span className="productName">{product.title}</span> </div> <div className="productInfoBottom"> <div className="productInfoItem"> <span className="productInfoKey">id:</span> <span className="productInfoValue">{product._id}</span> </div> <div className="productInfoItem"> <span className="productInfoKey">sales:</span> <span className="productInfoValue">5123</span> </div> <div className="productInfoItem"> <span className="productInfoKey">in stock:</span> <span className="productInfoValue">{product.inStock}</span> </div> </div> </div> </div> <div className="productBottom"> <form className="productForm"> <div className="productFormLeft"> <label>Product Name</label> <input type="text" placeholder={product.title} name="title" onChange={handleChange} /> <label>Product Description</label> <input type="text" placeholder={product.desc} name="desc" onChange={handleChange} /> <label>Product Price</label> <input type="text" placeholder={product.price} name="price" onChange={handleChange}/> <label>In Stock</label> <select name="inStock" id="idStock" onChange={handleChange} > <option value="true">Yes</option> <option value="false">No</option> </select> </div> <div className="productFormRight"> <div className="productUpload"> <img src={product.img} alt="" className="productUploadImg" /> <label for="file"> <Publish/> </label> <input type="file" id="file" style={{display:"none"}}onChange={(e) => setFile(e.target.files[0])} /> </div> <button className="productButton" onClick={handleClick}>Update</button> </div> </form> </div> </div> ); }
Redux更新逻辑
//UPDATE updateProductStart: (state) => { state.isFetching = true; state.error = false; }, updateProductSuccess: (state, action) => { state.isFetching = false; state.products[ state.products.findIndex((item) => item._id === action.payload.id) ] = action.payload.product; }, updateProductFailure: (state) => { state.isFetching = false; state.error = true; },
请求方法
import axios from "axios"; const BASE_URL = "http://localhost:5000/api/"; const user = JSON.parse(localStorage.getItem("persist:root"))?.user; const currentUser = user && JSON.parse(user).currentUser; const TOKEN = currentUser?.accessToken; export const publicRequest = axios.create({ baseURL: BASE_URL, }); export const userRequest = axios.create({ baseURL: BASE_URL, headers: { token: `Bearer ${TOKEN}` }, });
后端路由
//UPDATE router.put("/:id", verifyTokenAndAdmin, async (req, res) => { try { const updatedProduct = await Product.findByIdAndUpdate( req.params.id, { $set: req.body, }, { new: true } ); res.status(200).json(updatedProduct); } catch (err) { res.status(500).json(err); } });
Product Model
const mongoose = require("mongoose"); const ProductSchema = new mongoose.Schema( { title: { type: String, required: true, unique: true }, desc: { type: String, required: true }, img: { type: String, required: true }, categories: { type: Array }, size: { type: Array }, color: { type: Array }, price: { type: Number, required: true }, inStock: { type: Boolean, default: true }, }, { timestamps: true } ); module.exports = mongoose.model("Product", ProductSchema);
内容的提问来源于stack exchange,提问作者Juan
相关产品推荐
相关产品推荐

