API返回单条记录时React组件无法正常渲染的问题排查
问题分析与解决方案
核心错误点
状态类型不匹配
useFetch中products初始值是数组[],但获取单个产品时你将其设置为对象setProducts({ ...response.data }),导致:- 类型冲突(TypeScript严格模式下会直接报错)
CardDetail中products[0].title抛出Cannot read properties of undefined错误,因为此时products是对象而非数组
未处理异步加载边界
组件渲染时数据尚未返回,直接访问products[0].title必然报错,完全没处理加载中、数据为空的状态。hasMore逻辑错误
单个产品接口返回的是单条对象,不存在total字段,if (products.length < response.data.total)会抛出Cannot read property 'total' of undefined,且hasMore对单个请求无意义。id参数处理不严谨
useParams()返回的id可能为undefined,直接Number.parseInt(id)会得到NaN,导致无效请求。
修复方案:重构useFetch(推荐)
将列表和单个请求的逻辑拆分,用类型区分返回结果,覆盖所有边界状态:
import axios, { Canceler } from 'axios'; import { useEffect, useState } from 'react'; import { dummyProductType } from '../types/types'; // 定义返回类型,明确区分列表和单个数据 type UseFetchResult = | { type: 'list', data: dummyProductType[], isLoading: boolean, error: boolean, hasMore: boolean } | { type: 'single', data: dummyProductType | null, isLoading: boolean, error: boolean }; export const useFetch = ({ limit, id }: { limit?: number, id?: number }) => { const [data, setData] = useState<dummyProductType[] | dummyProductType | null>(null); const [isLoading, setIsLoading] = useState(false); const [hasMore, setHasMore] = useState(false); const [error, setError] = useState(false); useEffect(() => { let cancel: Canceler; let isListRequest = false; const config = id == null ? (isListRequest = true, { method: 'GET', url: 'https://dummyjson.com/products/', params: { limit }, cancelToken: new axios.CancelToken(c => cancel = c), }) : { method: 'GET', url: `https://dummyjson.com/products/${id}`, cancelToken: new axios.CancelToken(c => cancel = c), }; async function fetchData() { setIsLoading(true); setError(false); try { const response = await axios(config); if (isListRequest) { // 列表请求:追加数据适配无限滚动 setData(prev => [...(prev as dummyProductType[]), ...response.data.products]); // 判断是否还有更多数据 setHasMore( response.data.products.length > 0 && ((prev as dummyProductType[]).length + response.data.products.length) < response.data.total ); } else { // 单个请求:直接设置单条数据 setData(response.data); setHasMore(false); } } catch (err) { if (!axios.isCancel(err)) setError(true); } finally { setIsLoading(false); } } fetchData(); return () => cancel(); }, [limit, id]); // 根据请求类型返回对应结构 if (id != null) { return { type: 'single' as const, data: data as dummyProductType | null, isLoading, error, }; } else { return { type: 'list' as const, data: (data as dummyProductType[]) || [], isLoading, error, hasMore, }; } };
修复CardDetail组件
处理加载、错误、数据为空的边界状态:
import { Card, CardHeader, CardMedia, CardContent } from '@mui/material'; import { useParams } from 'react-router-dom'; import { useFetch } from '../../hooks/useFetch'; export const CardDetail = () => { const { id } = useParams(); // 处理id为undefined或非数字的情况 const productId = id ? Number.parseInt(id) : null; const { type, data, isLoading, error } = useFetch({ id: productId }); // 加载中状态 if (isLoading) return <div>加载中...</div>; // 错误或数据为空状态 if (error || !data) return <div>产品不存在或加载失败</div>; // 渲染单个产品数据 return ( <Card key={productId}> <CardHeader title={data.title} /> <CardMedia component='img' image={data.thumbnail} sx={{ height: '150px' }} /> <CardContent> <p>{data.description}</p> <p>价格: ${data.price}</p> </CardContent> </Card> ); };
最小修复方案(不重构useFetch)
如果不想大规模修改,可做以下调整:
- 在
useFetch的单个请求中,将数据包装为数组:
// 替换原单个请求的setProducts逻辑 setProducts([response.data]); setHasMore(false);
- 在
CardDetail中添加状态判断:
export const CardDetail = () => { const { id } = useParams(); const productId = id ? Number.parseInt(id) : null; const { products, isLoading, error } = useFetch({ id: productId }); if (isLoading) return <div>加载中...</div>; if (error || products.length === 0) return <div>产品不存在</div>; const product = products[0]; return ( <Card key={id}> <CardHeader title={product.title}></CardHeader> <CardMedia component='img' image={product.thumbnail} sx={{ height: '150px' }}></CardMedia> </Card> ); };
内容的提问来源于stack exchange,提问作者f4lk0N
相关产品推荐
相关产品推荐

