MERN栈直播拍卖应用MediaCard组件TypeError问题求助
直播拍卖应用MediaCard组件TypeError问题排查与解决
报错信息
Uncaught TypeError: Cannot read properties of undefined (reading 'image') at MediaCard (Card.js:53: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)
MediaCard组件代码
import * as React from "react"; import { connect } from "react-redux"; import Card from "@mui/material/Card"; import { Link, useNavigate } from "react-router-dom"; // Actions import { loadAdDetails, loadAdImage, setImageLoadingStatus, } from "../../actions/ad"; // MUI Components import { CardActionArea } from "@mui/material"; import CardContent from "@mui/material/CardContent"; import CardMedia from "@mui/material/CardMedia"; import Typography from "@mui/material/Typography"; // Files import imagePlaceholder from "../../images/no-image-icon.png"; import { secondsToHmsShort } from "../../utils/secondToHms"; function MediaCard(props) { const navigate = useNavigate(); const handleCardClick = (e) => { navigate(`/ads/${props.ad._id}`); }; // Auction status based on the ad-details const updateAuctionStatus = (ad) => { if (ad.sold) { return "Sold"; } else if (ad.auctionEnded) { return "Ended, not-sold"; } else if (!ad.auctionStarted) { return "Upcoming"; } else { return "Ongoing"; } }; return ( <a onClick={(e) => { handleCardClick(e); }} style={{ textDecoration: "none" }} > <Card style={props.cardStyle}> <CardActionArea> {!props.dashCard && ( <CardMedia component="img" height="180" src={props.ad.image ? props.ad.image : imagePlaceholder} alt="green iguana" /> )} <CardContent> <Typography gutterBottom variant="h6" component="div"> {props.ad.productName} </Typography> <Typography variant="body2" color="text.secondary"> Price: $ {props.ad.currentPrice.$numberDecimal} </Typography> <Typography variant="body2" color="text.secondary"> Remaining: {secondsToHmsShort(props.ad.timer)} </Typography> <Typography variant="body2" color="text.secondary"> Status: {updateAuctionStatus(props.ad)} </Typography> </CardContent> </CardActionArea> </Card> </a> ); } const mapStateToProps = (state) => ({ adDetails: state.ad.adDetails, }); export default connect(mapStateToProps, { loadAdDetails, loadAdImage, setImageLoadingStatus, })(MediaCard);
页面核心代码片段
return ( <div className="ad__page"> {props.loading ? ( <LoadingDisplay /> ) : ( <> <Alert /> {!props.adDetails.owner ? ( <LoadingDisplay /> ) : ( <Box sx={boxStyle}> <Paper sx={paperStyle}> <Typography variant="h4"> {props.adDetails.productName} </Typography> <Box sx={adArea}> <Box sx={imageContainer}> {!props.imageLoading && ( <img src={ props.adDetails.image ? props.adImage : imagePlaceholder } alt={props.adDetails.productName} style={imageStyle} /> )} </Box> <Box sx={descriptionArea}> <Typography variant="h6">Description</Typography> <Typography variant="body2"> {props.adDetails.description} </Typography> <Divider variant="middle" sx={{ margin: ".5rem" }} /> <Typography variant="h6">Info</Typography> <Typography variant="body1"> Posted on: {getUTCDate(props.adDetails.createdAt)} </Typography> <Typography variant="body1"> Seller: {props.adDetails.owner.username} </Typography> <Typography variant="body1"> Base price: {props.adDetails.basePrice.$numberDecimal} </Typography> <Divider variant="middle" sx={{ margin: ".5rem" }} /> <Typography variant="h6">Auction</Typography> <Typography variant="body1"> Status: {auctionStatus()} </Typography> <Typography variant="body1"> Bids: {props.adDetails.bids.length} </Typography> <Typography variant="body1"> Time remaining: {getTimeRemaining()} </Typography> <Typography variant="body1"> Current price: $ {props.adDetails.currentPrice.$numberDecimal} </Typography> <Typography variant="body1"> Current bidder: {props.highestBid && props.highestBid.user.username} </Typography> <Divider variant="middle" sx={{ margin: ".5rem" }} /> {!ownerAd && ( <Box sx={bidContainer}> <TextField label="$" id="bid-price" size="small" onChange={(e) => { handleBidPriceChange(e); }} /> <Box sx={{ height: "auto" }}> <Button variant="contained" disabled={bidButton} onClick={(e) => handleSubmitBid(e)} sx={bidButtonStyle} > Place bid </Button> </Box> </Box> )} {ownerAd && ( <Box sx={bidContainer}> <Box sx={{ height: "auto" }}> <Button variant="contained" disabled={!startButton} onClick={(e) => handleStartAuction(e)} sx={bidButton} > Start Auction </Button> </Box> </Box> )} </Box> </Box> </Paper> </Box> )} </> )} </div> );
问题原因
- props来源不匹配:MediaCard组件通过
mapStateToProps获取了adDetails,但组件内部一直使用props.ad,如果父组件没有传递ad属性,或者props.ad初始为undefined,就会触发报错。 - 缺少加载状态校验:MediaCard组件没有判断
props.ad是否存在就直接渲染,在数据未从后端加载完成时,props.ad还未被赋值,导致读取image等属性失败。
解决方法
方法一:修复props映射或使用正确的props
如果广告数据来自Redux的adDetails,修改mapStateToProps将其映射为ad,确保组件内部使用的props一致:
const mapStateToProps = (state) => ({ ad: state.ad.adDetails, });
或者直接在组件内将props.ad替换为props.adDetails,同时更新所有相关引用(比如props.adDetails._id、props.adDetails.image等)。
方法二:添加加载状态校验
在MediaCard组件渲染前,先判断props.ad是否存在,不存在则返回加载组件或空内容:
function MediaCard(props) { const navigate = useNavigate(); // 新增:判断ad是否存在,不存在则显示加载 if (!props.ad) { return <LoadingDisplay />; } const handleCardClick = (e) => { navigate(`/ads/${props.ad._id}`); }; // ... 其余代码不变 }
方法三:使用可选链操作符做防御性访问
在读取props.ad的属性时,添加可选链?.避免直接读取undefined的属性,这是临时规避报错的方案,建议配合加载校验一起使用:
// 修改CardMedia的src src={props.ad?.image ? props.ad.image : imagePlaceholder} // 修改其余属性引用 {props.ad?.productName} Price: $ {props.ad?.currentPrice?.$numberDecimal} Remaining: {secondsToHmsShort(props.ad?.timer)} Status: {updateAuctionStatus(props.ad)}
方法四:确保父组件在数据加载完成后再渲染MediaCard
如果ad是父组件传递的props,父组件需要在adDetails加载完成后再渲染MediaCard,比如:
// 父组件中渲染MediaCard的位置 {props.adDetails && <MediaCard ad={props.adDetails} />}
内容的提问来源于stack exchange,提问作者Dhruv verma
相关产品推荐
相关产品推荐

