React跳转商品详情页报reading 'details' undefined错误
问题现象
实现商品卡片点击跳转详情页功能时,从浏览器返回上一页后,点击其他商品卡片跳转对应详情页,会抛出类型错误:TypeError: Cannot read properties of undefined (reading 'details')
已知details为允许为空的数组类型,代码中已添加product['details']真值判断,预期仅当属性存在时渲染对应内容,但判断逻辑未生效,报错依然触发。
相关问题代码如下:
const [ dataLoad, holdRender ] = useState(false); //hold render until data is ready const [ product, setProduct ] = useState({}); const [ currentProduct, setCurrentProduct ] = useState(0); //this useEffect runs once useEffect(()=> { (async () => { //set params id received so whenever it updates the data is updated const productId = await props.getProduct(props.match.params.id); setCurrentProduct(productId); })(); }, []); //this useEffect runs everytime the params id is received/changed useEffect(() => { //get data from the backend to update UI in time until then hold (async () => { const data = await props.security.product.data; setProduct(data); })(); //if product.details is now available if(product['details']){ //let go of render holdRender(true); } else{ return null; } return () => { setCurrentProduct(0); }; }, [currentProduct]); return( <div> {dataLoad !== false? <div> <Row> <Col> <Jumbotron id="productStatsCard"> <div className="productStats"> <h2>{product? product['details_not_verified']: <Loader/>}</h2> </div> </Jumbotron> </Col> <Col> <Jumbotron id="productStatsCard"> <div className="productStats"> <h2>{product? product['details_verified']: <Loader />}</h2> </div> </Jumbotron> </Col> </Row> <Row className="mt-5"> <Col sm={8}> <Row> <Col> <div className="pdtTableContainer"> <div> <Table> <thead> <tr> <th>Category</th> <th>Product Id</th> <th>Product Name</th> <th>Product Type</th> </tr> </thead> <tbody> {dataLoad && product['details']? product['details'].map(detail => ( <tr key={product.id}> <td> {product.category} </td> <td>{product.id}</td> <td> {product.product_title} </td> <td>{product.product_type}</td> </tr> )) : 'No products to display'} </tbody> </Table> </div> </div> </Col> </Row> </Col> </Row> </div>: <Loader/>} </div> ) } const mapStateToProps = (state) => ({ security: state.security, }); export default connect(mapStateToProps, { getProduct })(productOverview);
根因分析
- 状态更新时序错误:React的
setState是异步批量更新的,调用setProduct(data)后同步读取product变量,拿到的还是当前闭包里缓存的旧状态,不是刚请求回来的新数据。此时判断product['details']完全是基于旧值判断,会把dataLoad错误设置为true,但新的product数据还没完成赋值,可能为undefined。 - 状态setter命名错误:
const [ dataLoad, holdRender ] = useState(false)中第二个返回值是修改dataLoad的方法,命名为holdRender和实际功能完全相反,极易写反控制逻辑。 - useEffect逻辑错误:清理函数中
setCurrentProduct(0)会在依赖项变化、组件卸载时触发,返回上一页再进入新详情页时,会触发状态更新竞态,旧状态被清空但加载标记未同步重置;同时依赖数组缺失props.match.params.id、props.security.product.data等内部使用的变量,违反React Hook依赖规则,长期捕获闭包旧值。 - 渲染层防护不足:仅判断了
product['details'],未先校验product本身是否存在,当product被重置为undefined时,直接读取product['details']就会触发类型错误;同时未校验details是否为数组,空值场景下直接调用map也会报错。 - 列表key错误:遍历
details时用product.id作为key,所有列表项key重复,会导致React渲染复用异常。
修复方案
- 修正状态setter命名,切换商品时先重置加载状态和旧商品数据,避免旧数据渲染
- 调整状态更新时序,等接口数据赋值到product状态后,再设置加载完成标记
- 增加挂载标记,避免组件卸载后更新状态导致内存泄漏和竞态问题
- 补全useEffect依赖数组,删除有害的清理函数逻辑
- 渲染层增加多层空值防护,用可选链、数组类型校验兜底异常场景
- 修正列表项key,用详情项唯一ID或索引兜底
修复后的完整代码:
const [ dataLoad, setDataLoad ] = useState(false); const [ product, setProduct ] = useState({}); const [ currentProduct, setCurrentProduct ] = useState(0); // 路由商品ID变化时同步更新currentProduct useEffect(()=> { (async () => { const productId = await props.getProduct(props.match.params.id); setCurrentProduct(productId); })(); }, [props.match.params.id, props.getProduct]); // 商品ID变化时拉取对应商品详情 useEffect(() => { // 切换商品先重置状态 setDataLoad(false); setProduct({}); let isMounted = true; (async () => { const data = await props.security.product.data; if(isMounted){ setProduct(data ?? {}); setDataLoad(true); } })(); return () => { isMounted = false; }; }, [currentProduct, props.security.product.data]); return( <div> {dataLoad && product ? <div> <Row> <Col> <Jumbotron id="productStatsCard"> <div className="productStats"> <h2>{product?.details_not_verified ?? <Loader/>}</h2> </div> </Jumbotron> </Col> <Col> <Jumbotron id="productStatsCard"> <div className="productStats"> <h2>{product?.details_verified ?? <Loader />}</h2> </div> </Jumbotron> </Col> </Row> <Row className="mt-5"> <Col sm={8}> <Row> <Col> <div className="pdtTableContainer"> <div> <Table> <thead> <tr> <th>Category</th> <th>Product Id</th> <th>Product Name</th> <th>Product Type</th> </tr> </thead> <tbody> {Array.isArray(product?.details) && product.details.length > 0 ? product.details.map((detail, index) => ( <tr key={detail?.id ?? index}> <td>{product.category}</td> <td>{product.id}</td> <td>{product.product_title}</td> <td>{product.product_type}</td> </tr> )) : 'No products to display' } </tbody> </Table> </div> </div> </Col> </Row> </Col> </Row> </div>: <Loader/>} </div> ) } const mapStateToProps = (state) => ({ security: state.security, }); export default connect(mapStateToProps, { getProduct })(productOverview);
内容的提问来源于stack exchange,提问作者user8210845
相关产品推荐
相关产品推荐

