本文将帮助你彻底理解 React 函数组件中因状态更新触发无限重渲染,导致 axios 请求反复执行的根本原因。核心解决方案是使用 useEffect 搭配空依赖数组,确保异步初始化逻辑仅在组件挂载时执行一次,同时附上进度管理与缓存优化的实战经验。
在 React 函数组件中,直接在函数体顶层调用异步请求(例如 axios.get(...))是一种看似简便、实则隐患巨大的写法。问题在于:每次状态更新(比如调用 setProgress)都会触发组件重新渲染,而重新渲染又会导致该请求再次执行,形成死循环,最终耗尽浏览器资源甚至导致页面崩溃。你代码中遇到的根源正是:axios.get(...).then(...) 没有被约束执行时机,且循环中频繁调用 setProgress(progress + 1) 引发了连续重渲染。
正确的做法其实很简单:将数据获取逻辑放入 useEffect Hook,并传入空依赖数组 [],明确告诉 React——“只在组件首次挂载时运行一次,其他时候不要触碰”。
import './App.css';
import axios from 'axios';
import React, { useState, useEffect } from 'react';
function App() {
const [allDataJson, setAllData] = useState(null);
const [progress, setProgress] = useState(0);
useEffect(() => {
// ✅ 仅在组件挂载时执行一次
const fetchData = async () => {
try {
// 第一步:获取 Pokémon 列表(支持分页)
let allResults = [];
let url = "https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20";
while (url) {
const response = await axios.get(url);
allResults = [...allResults, ...response.data.results];
url = response.data.next; // 继续下一页,直到 next 为 null
}
// 第二步:并发获取详情(推荐 Promise.all 提升性能)
const detailPromises = allResults.map(async (item) => ({
...item,
detail: await getDetails(item.url)
}));
// 使用 Promise.all 并行加载,避免串行阻塞
const fullData = await Promise.all(detailPromises);
// ✅ 批量更新状态,减少渲染次数
setAllData(JSON.stringify(fullData));
} catch (error) {
console.error("Failed to fetch data:", error);
}
};
fetchData();
}, []); // ? 空数组确保只运行一次
return (
{allDataJson ? (
✅ Completed Load
) : (
Loading, please wait... {progress}/20
)}
);
}
// ✅ 优化后的缓存细节(修复原 cache 全局污染风险)
const cache = new Map();
export async function getDetails(url) {
const id = url.split('/').filter(Boolean).at(-2); // 更健壮地提取 ID
const cacheKey = `pokemon-${id}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const response = await axios.get(url);
cache.set(cacheKey, response.data);
return response.data;
}
export default App;
关键注意事项
- 禁止在渲染体中直接调用副作用:所有数据获取、订阅、定时器等操作,必须封装到
useEffect中,切勿图省事直接写在函数体里。 - 避免在循环中逐个
await+setState:每次setState都会触发一次重渲染,循环中这么做等于自毁。正确做法是先用Promise.all()并行请求,最后统一调用setState。 - useEffect 的依赖数组必须严谨:
[]表示无依赖,仅在挂载/卸载时触发;若要响应props或state变化,必须显式列出依赖项,不可遗漏或随意填写。 - 缓存推荐使用
Map或WeakMap:比普通对象更安全,能避免原型污染,且支持任意键类型,使用起来更灵活。 - 错误处理不可省略:生产环境务必包裹
try/catch,否则一个 Promise 拒绝就可能导致页面白屏,用户体验极差。
进阶提示
如果你需要真实的进度条(而非简单的计数),可以考虑结合 useReducer 来管理加载状态,或者使用 React.Suspense + 自定义 Hook 封装异步逻辑。这样不仅能提升代码清晰度,还能显著改善可维护性与用户体验。
