循环内嵌套Axios调用首次渲染不展示数据问题排查
问题描述
组件会发起三次不同的Axios请求,其中getProductsByIds和getLegalNamesById这两次请求在getAllSearchResults请求返回结果的循环内部调用。组件首次渲染完成后仅展示getAllSearchResults请求的数据,另外两个请求的数据需在与组件交互(如点击)后才会渲染,不清楚问题出在哪里?
代码实现如下:
let panels = {documentSource: {summary: 'Document Source', content: new Set()}, legalName: {summary: 'Legal Name', content: new Set()}, productName: {summary: 'Product Name', content: new Set()},}; const RenderData = () => { const [accordionResults, setAccordionResults] = useState(null); const [isLoadingLegalName, setIsLoadingLegalName] = useState(true); const [isLoadingProductName, setIsLoadingProductName] = useState(true); useEffect(() => { fetchResults(); }, []); const fetchResults = () => { return searchService() .getAllSearchResults("search", 650) .then((response) => { let results = response.data.results; if (results.length > 0) { Promise.all(results.map((obj, index) => { panels.documentSource.content.add(obj.A.value[0]); getLegalName(obj.B.value[0]); getProductName(obj.C.value[0]); })) } setAccordionResults(panels); }).catch((error) => { console.log(error); }); }; const getLegalName = (id) => { return CommonRefService() .getLegalNamesById([id]) .then((response) => { const legalName = response.data.results[0]; panels.legalName.content.add(legalName.name + ' - ' + legalName.id); setIsLoadingLegalName(false); }).catch((error) => { console.log(error); }); }; const getProductName = (id) => { return commonService() .getProductsByIds([id]) .then((response) => { const productName = response.data[0].prodType; panels.productName.content.add(productName); setIsLoadingProductName(false); }).catch((error) => { console.log(error); }); }; if(isLoadingProductName || isLoadingLegalName) { return <div>Loading...</div>; } }
问题分析与修复
核心问题点
- 直接修改外部非状态变量:
panels是组件外部的普通对象,直接修改它的content集合不会触发React组件的重新渲染。React只有在状态(useState/useReducer)更新时才会重新渲染组件,所以即使后续请求拿到数据,组件也不会自动更新视图,只有交互触发重新渲染后才会显示新数据。 - Promise.all未正确收集异步请求:当前
Promise.all里的map返回的是undefined(因为内部没有返回getLegalName和getProductName的Promise),导致setAccordionResults会在getAllSearchResults完成后立即执行,此时后两个请求还没返回数据,panels里的对应内容是空的。 - Loading状态管理错误:多个请求场景下,只要第一个
getLegalName或getProductName完成就把对应的loading设为false,但其他请求可能还在进行,会导致loading提前结束但数据未完全加载。 - 状态更新方式错误:即使
panels是状态,直接修改Set的内容(add方法)属于修改引用类型的内部值,React无法检测到变化,不会触发重新渲染。
修复后的代码
const RenderData = () => { // 初始化状态为包含空Set的结构,替代外部的panels变量 const [accordionResults, setAccordionResults] = useState({ documentSource: { summary: 'Document Source', content: new Set() }, legalName: { summary: 'Legal Name', content: new Set() }, productName: { summary: 'Product Name', content: new Set() }, }); // 用请求计数来管理loading,避免单个请求完成就关闭loading const [pendingRequests, setPendingRequests] = useState(0); useEffect(() => { fetchResults(); }, []); const fetchResults = () => { searchService() .getAllSearchResults("search", 650) .then((response) => { const results = response.data.results; if (results.length === 0) { setAccordionResults(prev => ({...prev})); return; } // 计算需要发起的总请求数(每个result对应2个请求) setPendingRequests(results.length * 2); // 收集所有异步请求的Promise const allRequests = results.flatMap((obj) => { // 先更新documentSource数据,创建新的Set避免直接修改状态 setAccordionResults(prev => ({ ...prev, documentSource: { ...prev.documentSource, content: new Set(prev.documentSource.content).add(obj.A.value[0]) } })); // 返回两个请求的Promise return [ getLegalName(obj.B.value[0]), getProductName(obj.C.value[0]) ]; }); // 等待所有请求完成 Promise.all(allRequests).then(() => { // 所有请求完成,可做后续处理 }).catch(err => console.log(err)); }) .catch((error) => { console.log(error); setPendingRequests(0); }); }; const getLegalName = (id) => { return CommonRefService() .getLegalNamesById([id]) .then((response) => { const legalName = response.data.results[0]; const newLegalName = `${legalName.name} - ${legalName.id}`; // 创建新的Set并更新状态,触发重新渲染 setAccordionResults(prev => ({ ...prev, legalName: { ...prev.legalName, content: new Set(prev.legalName.content).add(newLegalName) } })); // 完成一个请求,计数减1 setPendingRequests(prev => prev - 1); }) .catch((error) => { console.log(error); setPendingRequests(prev => prev - 1); }); }; const getProductName = (id) => { return commonService() .getProductsByIds([id]) .then((response) => { const productName = response.data[0].prodType; // 创建新的Set并更新状态,触发重新渲染 setAccordionResults(prev => ({ ...prev, productName: { ...prev.productName, content: new Set(prev.productName.content).add(productName) } })); // 完成一个请求,计数减1 setPendingRequests(prev => prev - 1); }) .catch((error) => { console.log(error); setPendingRequests(prev => prev - 1); }); }; // 当还有未完成的请求时显示loading if (pendingRequests > 0) { return <div>Loading...</div>; } // 这里补充你的渲染逻辑,比如渲染accordionResults return ( <div> {/* 示例渲染逻辑 */} {Object.values(accordionResults).map(panel => ( <div key={panel.summary}> <h3>{panel.summary}</h3> <ul> {Array.from(panel.content).map(item => ( <li key={item}>{item}</li> ))} </ul> </div> ))} </div> ); };
关键修改说明
- 将
panels改为组件内部的状态,所有数据更新通过setAccordionResults完成,确保触发重新渲染。 - 使用
pendingRequests计数管理loading状态,只有所有请求完成后才隐藏loading。 - 更新Set时创建新的Set实例(
new Set(prev.content).add(item)),避免直接修改原状态的引用类型,确保React能检测到状态变化。 - 正确收集所有异步请求的Promise到
allRequests数组,用Promise.all等待所有请求完成。
内容的提问来源于stack exchange,提问作者kobe
相关产品推荐
相关产品推荐

