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

电商项目获取单商品数据时的报错问题求助

问题:电商单商品页面数据加载报错

开发电商项目时,通过API根据ID获取单条商品数据的功能,在以下场景出现报错:

  • 刷新页面
  • 快速跳转至该页面
  • 在单商品页面点击更新Redux状态的按钮

报错截图:
报错信息截图

已尝试设置初始对象避免报错,但未生效,恳请帮忙解决。

相关代码

singleListingSlice.js

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { toast } from "react-toastify"
import customFetch from "../../utils/axios"

const initialState = {
  isLoading: false,
  isError: false,
  createSuccess: false,
  listing: {},
}

export const getSingleListing = createAsyncThunk(
  "singleListing/getSingleListing",
  async (id, thunkAPI) => {
    try {
      const { data } = await customFetch(`/listing/${id}`)
      return data.listing
    } catch (error) {
      return thunkAPI.rejectWithValue(error.response.data.msg)
    }
  }
)

const singleListingSlice = createSlice({
  name: "singleListing",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(getSingleListing.pending, (state) => {
        state.isLoading = true
      })
      .addCase(getSingleListing.fulfilled, (state, { payload }) => {
        state.isLoading = false
        state.listing = payload
        toast.success(`done fetching`, {
          toastId: "success",
        })
      })
      .addCase(getSingleListing.rejected, (state) => {
        state.isLoading = false
        state.isError = true
        toast.error(`can't find listing, please try again`)
      })
  },
})

export default singleListingSlice.reducer

singleListingPage.jsx

import { useState, useEffect } from "react"
import { useSelector, useDispatch } from "react-redux"
import { useParams } from "react-router-dom"

import {
  Searchbox,
  UserInfo,
  ReviewCard,
  OfferBox,
  Slider,
} from "../components"
import { getSingleProduct } from "../features/singleProduct/singleProductSlice"

const initialState = {
  condition: "",
  createdAt: "",
  createdBy: "",
  dealMethod: {
    meetUp: true,
    delivery: false,
  },
  description: "",
  meetUpLocation: "",
  name: "",
  photos: [],
  previousPrice: "",
  price: "",
  soldTo: "",
  status: "",
  updatedAt: "",
  _id: "",
}
const SingleListingPage = () => {
  const [listing, setListing] = useState(initialState)
  const { user } = useSelector((store) => store.user)
  const {
    listing: storeListing,
    isLoading,
    isError,
  } = useSelector((store) => store.singleListing)
  const { id: listingId } = useParams()
  const dispatch = useDispatch()

  useEffect(() => {
    dispatch(getSingleListing(listingId))
  }, [listingId])

  useEffect(() => {
    setListing(storeListing)
  }, [storeListing])

  if (isLoading) {
    return <h1>Loading...</h1>
  }

  if (isError) {
    return <h1>Error</h1>
  }

  const {
    condition,
    createdAt,
    createdBy,
    dealMethod,
    description,
    meetUpLocation,
    name,
    numOfFavorite,
    photos,
    previousPrice,
    price,
    soldTo,
    status,
    updatedAt,
    _id,
  } = listing

  let tradeMethod = []
  Object.entries(dealMethod).forEach((method) => {
    if (!method[1]) return
    tradeMethod = [...tradeMethod, method[0]]
  })

  return (
    <Wrapper>
      <div className="container">
        <Searchbox />
        <div className="bread-crumb">category / {name}</div>
        <SingleListingSlider photos={photos} />
        <section className="listing-information">
          <div className="details">
            <div className="row">
              <h3 className="name">{name}</h3>
              <div className="favorite">
                {numOfFavorite > 0 ? <FaRegHeart /> : <FaHeart />}
                <span>{numOfFavorite}</span>
              </div>
            </div>
            <h2 className="price">HK${price}</h2>
            <span>condition: {condition}</span>
            {/* <span>trade method: {tradeMethod}</span> */}
            <span>location: {meetUpLocation}</span>
          </div>
          <OfferBox />
        </section>
        <section className="section-description">
          <h2 className="title">Description</h2>
          <article>{description}</article>
        </section>
        <section className="section-review">
          <div className="col">
            <UserInfo {...user} />
          </div>
          <div className="col">
            <h2 className="title">reviews for @{name}</h2>
            <div className="reviews">
              <ReviewCard />
              <ReviewCard />
              <ReviewCard />
            </div>
          </div>
        </section>
      </div>
    </Wrapper>
  )
}

export default SingleListingPage

问题分析与解决方案

核心问题

  1. Redux初始状态的listing是空对象,且缺少numOfFavorite等组件需要的字段,数据加载前解构空对象会触发报错。
  2. 组件维护了本地listing状态,和Redux状态同步时,会被空的storeListing覆盖,丢失初始默认值。
  3. 快速操作时,isLoading状态可能未及时拦截渲染,导致提前访问未加载的属性。

修复步骤

1. 同步Redux初始状态字段

把组件里的完整初始对象同步到Redux的initialState中,确保所有需要解构的字段都有默认值:

// singleListingSlice.js
const initialState = {
  isLoading: false,
  isError: false,
  createSuccess: false,
  listing: {
    condition: "",
    createdAt: "",
    createdBy: "",
    dealMethod: {
      meetUp: true,
      delivery: false,
    },
    description: "",
    meetUpLocation: "",
    name: "",
    numOfFavorite: 0, // 补上缺失字段
    photos: [],
    previousPrice: "",
    price: "",
    soldTo: "",
    status: "",
    updatedAt: "",
    _id: "",
  },
}

2. 移除本地状态,直接使用Redux状态

不需要维护本地listing,直接用Redux的storeListing,避免状态同步冲突:

// singleListingPage.jsx
// 删除本地状态相关代码:
// const initialState = {...}
// const [listing, setListing] = useState(initialState)
// useEffect(() => { setListing(storeListing) }, [storeListing])

// 直接解构Redux状态
const {
  condition,
  createdAt,
  ... // 其他字段
  numOfFavorite,
} = storeListing

3. 可选:添加兜底校验

在JSX中使用可选链操作符,避免极端情况的空值报错:

{numOfFavorite?.toString() || '0'}
<SingleListingSlider photos={photos || []} />

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 05:37:59