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)
首页截图

问题分析与修复方案
核心问题
- Redux状态冲突:
fetchAllProducts将state.products设为商品数组,而getSingleProduct将其设为单个商品对象。当首页加载列表后进入详情页,状态被覆盖为单个商品;但后续回到首页重新加载列表,再进入详情页时,状态可能变回数组,导致products.product为undefined触发报错。 - 缺少空值保护:组件渲染时未对
products.product做足够判断,数据未加载或状态异常时直接报错。
修复步骤
拆分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;更新组件状态读取逻辑
在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
相关产品推荐
相关产品推荐

