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

React18+Redux Toolkit商品详情页数据异常及undefined报错求助

问题描述

我使用ReactJS 18、Redux Toolkit和Node.js 16开发项目,点击任意商品进入ProductDetails页面时,首次点击能正常获取该商品ID的全部数据,但一段时间后返回空数组,同时触发如下错误:

ProductDetails.jsx:56 Uncaught TypeError: Cannot read properties of undefined (reading 'images')

相关代码

ProductDetails.jsx

import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import MetaData from '../../more/MetaData'
import { getSingleProduct, STATUSES } from '../../store/slices/productSlice';
import Header from '../Home/Header'
import { useParams } from "react-router-dom";
import loading from "../../assets/loading.svg"
import Slider from "react-slick";
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import ShoppingBagOutlinedIcon from '@mui/icons-material/ShoppingBagOutlined';

const ProductDetails = () => {
    const dispatch = useDispatch();
    const { id } = useParams()
    const { products, status } = useSelector((state) => state.product)
    console.log(products);

    const slider = {
        dots: true,
        infinite: true,
        slidesToShow: 1,
        slidesToScroll: 1,
        autoplay: true,
        speed: 3000,
        autoplaySpeed: 6000,
        cssEase: "linear"
    };

    useEffect(() => {
        const fetchProduct = () => {
            const action = getSingleProduct(id?.toString())
            dispatch(action)
        }
        fetchProduct()
    }, [dispatch, id])
    console.log(products.product);

    if (status === STATUSES.LOADING) {
        return (
            <div className="w-full h-[100vh] flex justify-center items-center bg-gray-100">
                 <img src={loading} alt="" className="bg-transparent rounded-full h-[300px]" />
            </div>
        );
    }
    return (
        <>
              <MetaData title="Product Details" />
              <Header />
              <div className="w-full flex justify-around md:flex-row flex-col">
                  <div>
                      <Slider {...slider}>
                          <div>
                              {products.product.images && 
                                  products.product.images.map((item, i) => (
                                      <img
                                       key={i}
                                       src={item.url}
                                       alt={`${i} Slide`}
                                       className="w-[350px] h-[350px] object-contain"
                                      />
                                ))}
                        </div>
                    </Slider>
                </div>
               <div>
              <h2>{products.product.name}</h2>
              <div>
                  <span>({products.product.numOfReviews})</span>
              </div>
              <h2>₹{products.product.price}</h2>
              <div>
                  <span>Quantity</span>
                  <div>
                      <button>-</button>
                      <input type="number" />
                      <button>+</button>
                  </div>
              </div>
              <p>{products.product.stock < 1 ? "OutOfStock" : "InStock"}</p>
              <div>
                  {products.product.description}
              </div>
              <div>
                  <div>
                      <FavoriteBorderIcon />
                      <span>Add to wishlist</span>
                  </div>
                  <div>
                      <ShoppingBagOutlinedIcon />
                      <span>Add to Cart</span>
                  </div>
              </div>
          </div>
      </div>
    </>
 )
}

export default ProductDetails

productSlice.js

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import * as api from "../api";

export const STATUSES = Object.freeze({
    IDLE: 'idle',
    ERROR: 'error',
    LOADING: 'loading',
});

// Thunk
export const fetchAllProducts = createAsyncThunk("products/getAllProduct", async (_, thunkAPI) => {
    try {
        const response = await api.getAllProducts()
        return response.data
    }catch(error){
        return thunkAPI.rejectWithValue(error.response.data)
    }
})

export const getSingleProduct  = createAsyncThunk("product/getSingleProduct", async (id, { rejectWithValue }) => {
    try {
        const response = await api.getSingleProduct(id)
        console.log(response.data);
        return await response.data
    }catch(error){
        console.log(error.response)
        return rejectWithValue(error.response)
    }
})

const productSlice = createSlice({
    name: "product",
    initialState: {
        products: [],
        status: STATUSES.IDLE,
        message: ""
    },
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(fetchAllProducts.pending, (state, action) => {
                state.status = STATUSES.LOADING
            })
            .addCase(fetchAllProducts.fulfilled, (state, action) => {
                state.products = action.payload
                state.status = STATUSES.IDLE
            })
            .addCase(fetchAllProducts.rejected, (state, action) => {
                state.status = STATUSES.ERROR
                state.message = action.payload.message
             })
             .addCase(getSingleProduct.pending, (state, action) => {
                 state.status = STATUSES.LOADING
              })
             .addCase(getSingleProduct.fulfilled, (state, action) => {
                 state.products = action.payload
                 state.status = STATUSES.IDLE
              })
            .addCase(getSingleProduct.rejected, (state, action) => {
                 state.status = STATUSES.ERROR
                 state.message = action.payload.message
              })
    }
})

export default productSlice.reducer

store.js

import { configureStore } from "@reduxjs/toolkit";
import productSlice from "./slices/productSlice";

const store = configureStore({
    reducer: {
        product: productSlice
    }
})

export default store

api.js

import axios from 'axios';

const API = axios.create({
    baseURL: "http://localhost:5000",
    headers: {
        'Content-Type': 'application/json',
    },
})

export const getAllProducts = async () => await API.get("/api/v2/products")
export const getSingleProduct = async (id) => await API.get(`/api/v2/products/${id}`)

错误栈

ProductDetails.jsx:56 Uncaught TypeError: Cannot read properties of undefined 
 (reading 'images')
 at ProductDetails (ProductDetails.jsx:56:1)
 at renderWithHooks (react-dom.development.js:16305:1)
 at mountIndeterminateComponent (react-dom.development.js:20074:1)
 at beginWork (react-dom.development.js:21587:1)
 at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1)
 at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1)
 at invokeGuardedCallback (react-dom.development.js:4277:1)
 at beginWork$1 (react-dom.development.js:27451:1)
 at performUnitOfWork (react-dom.development.js:26557:1)
 at workLoopSync (react-dom.development.js:26466:1)

首页截图

首页截图


问题分析与修复方案

核心问题

  1. Redux状态冲突:fetchAllProducts将state.products设为商品数组,而getSingleProduct将其设为单个商品对象。当首页加载列表后进入详情页,状态被覆盖为单个商品;但后续回到首页重新加载列表,再进入详情页时,状态可能变回数组,导致products.product为undefined触发报错。
  2. 缺少空值保护:组件渲染时未对products.product做足够判断,数据未加载或状态异常时直接报错。

修复步骤

  1. 拆分Redux状态结构
    修改productSlice,将商品列表和单个商品详情分开存储,避免互相覆盖:

    const productSlice = createSlice({
        name: "product",
        initialState: {
            productList: [], // 存储商品列表
            singleProduct: null, // 存储单个商品详情
            status: STATUSES.IDLE,
            message: ""
        },
        reducers: {
            resetSingleProduct: (state) => {
                state.singleProduct = null;
            }
        },
        extraReducers: (builder) => {
            builder
                // 处理商品列表请求
                .addCase(fetchAllProducts.pending, (state) => {
                    state.status = STATUSES.LOADING;
                })
                .addCase(fetchAllProducts.fulfilled, (state, action) => {
                    state.productList = action.payload;
                    state.status = STATUSES.IDLE;
                })
                .addCase(fetchAllProducts.rejected, (state, action) => {
                    state.status = STATUSES.ERROR;
                    state.message = action.payload.message;
                })
                // 处理单个商品请求
                .addCase(getSingleProduct.pending, (state) => {
                    state.status = STATUSES.LOADING;
                })
                .addCase(getSingleProduct.fulfilled, (state, action) => {
                    state.singleProduct = action.payload;
                    state.status = STATUSES.IDLE;
                })
                .addCase(getSingleProduct.rejected, (state, action) => {
                    state.status = STATUSES.ERROR;
                    state.message = action.payload.message;
                });
        }
    });
    
    export const { resetSingleProduct } = productSlice.actions;
    export default productSlice.reducer;
    
  2. 更新组件状态读取逻辑
    在ProductDetails.jsx中,改为读取singleProduct并添加空值保护:

    const { singleProduct, status } = useSelector((state) => state.product);
    
    useEffect(() => {
        const fetchProduct = () => {
            dispatch(resetSingleProduct());
            dispatch(getSingleProduct(id?.toString()));
        };
        fetchProduct();
    }, [dispatch, id]);
    
    if (status === STATUSES.LOADING) {
        return (
            <div className="w-full h-[100vh] flex justify-center items-center bg-gray-100">
                 <img src={loading} alt="" className="bg-transparent rounded-full h-[300px]" />
            </div>
        );
    }
    
    // 空值判断,避免无数据时渲染报错
    if (!singleProduct) {
        return <div className="w-full h-[100vh] flex justify-center items-center">商品数据加载失败</div>;
    }
    
    // 渲染部分替换所有products.product为singleProduct,可配合可选链增强容错
    return (
        <>
            <MetaData title="Product Details" />
            <Header />
            <div className="w-full flex justify-around md:flex-row flex-col">
                <div>
                    <Slider {...slider}>
                        <div>
                            {singleProduct.images?.map((item, i) => (
                                <img
                                    key={i}
                                    src={item.url}
                                    alt={`${i} Slide`}
                                    className="w-[350px] h-[350px] object-contain"
                                />
                            ))}
                        </div>
                    </Slider>
                </div>
                <div>
                    <h2>{singleProduct?.name}</h2>
                    <div>
                        <span>({singleProduct?.numOfReviews})</span>
                    </div>
                    <h2>₹{singleProduct?.price}</h2>
                    <div>
                        <span>Quantity</span>
                        <div>
                            <button>-</button>
                            <input type="number" />
                            <button>+</button>
                        </div>
                    </div>
                    <p>{singleProduct?.stock < 1 ? "OutOfStock" : "InStock"}</p>
                    <div>{singleProduct?.description}</div>
                    <div>
                        <div>
                            <FavoriteBorderIcon />
                            <span>Add to wishlist</span>
                        </div>
                        <div>
                            <ShoppingBagOutlinedIcon />
                            <span>Add to Cart</span>
                        </div>
                    </div>
                </div>
            </div>
        </>
    );
    

内容的提问来源于stack exchange,提问作者Pavitar Sharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 17:16:01