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

更新产品时出现TypeError: Cannot read properties of undefined (reading 'img')错误

我在更新产品时,执行操作后出现如下错误:
TypeError: Cannot read properties of undefined (reading 'img')
但产品实际已更新成功,离开当前页面再返回(不刷新)能看到带正确图片的产品。

问题原因

错误直接指向product.img的访问,核心是组件渲染时product对象为undefined,触发场景分两种:

  1. 组件初始化阶段:Redux中的products列表还未完成加载,find方法找不到对应产品返回undefined,直接访问属性就报错。
  2. 产品更新后: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 14:39:17