React函数组件Axios重复调用致浏览器崩溃问题求助
解决React组件重复调用API导致崩溃的问题 + 进度条实现
核心问题分析
你的问题根源在于API调用逻辑没有被正确限制执行时机:函数组件每次重渲染都会从头执行代码,如果API调用直接写在组件顶层,每次状态更新(比如更新progress)都会触发重渲染,进而再次调用API,形成无限循环,最终导致浏览器崩溃。全局变量started无法被React追踪,反而会破坏组件状态的一致性,导致渲染异常。
解决方案步骤
1. 用useEffect控制API调用时机
将所有API调用逻辑放到useEffect钩子中,并设置空依赖数组[],确保只在组件首次挂载时执行一次。同时用isLoading状态标记加载状态,防止重复触发调用。
2. 安全更新进度状态
更新进度时使用函数式更新(setProgress(prev => prev + step)),避免依赖外部变量导致的状态不同步;或者用useRef跟踪当前完成的请求数,减少不必要的重渲染。
3. 批量处理详情API调用
用Promise.all或Promise.allSettled批量请求详情数据,同时逐个更新进度。注意用Axios的取消令牌(CancelToken)在组件卸载时取消未完成的请求,防止内存泄漏。
4. 实现简易进度条
通过状态控制进度条的宽度,结合加载状态展示UI,让用户清晰看到加载进度。
完整代码示例
import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; function App() { const [pokemonList, setPokemonList] = useState([]); const [progress, setProgress] = useState(0); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(''); const cancelTokenRef = useRef(null); // 用于取消Axios请求 useEffect(() => { // 创建取消令牌 cancelTokenRef.current = axios.CancelToken.source(); const fetchPokemonData = async () => { try { // 1. 获取汇总数据 const summaryRes = await axios.get('https://pokeapi.co/api/v2/pokemon?limit=20', { cancelToken: cancelTokenRef.current.token }); const pokemonUrls = summaryRes.data.results.map(item => item.url); const total = pokemonUrls.length; let completed = 0; // 2. 批量请求详情数据,逐个更新进度 const pokemonPromises = pokemonUrls.map(async (url) => { const detailRes = await axios.get(url, { cancelToken: cancelTokenRef.current.token }); // 每完成一个请求就更新进度 completed++; setProgress(Math.round((completed / total) * 100)); return detailRes.data; }); // 等待所有请求完成 const detailedPokemon = await Promise.all(pokemonPromises); setPokemonList(detailedPokemon); } catch (err) { if (!axios.isCancel(err)) { setError('加载数据失败,请稍后重试'); } } finally { setIsLoading(false); setProgress(100); // 加载完成后进度设为100% } }; // 只有在未加载时才执行 if (isLoading) { fetchPokemonData(); } // 组件卸载时取消请求 return () => { if (cancelTokenRef.current) { cancelTokenRef.current.cancel('组件已卸载,取消请求'); } }; }, [isLoading]); // 依赖isLoading,确保状态变化时能正确控制 // 渲染UI return ( <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}> <h1>Pokemon列表</h1> {/* 进度条 */} {isLoading && ( <div style={{ height: '8px', backgroundColor: '#eee', borderRadius: '4px', marginBottom: '20px' }}> <div style={{ height: '100%', width: `${progress}%`, backgroundColor: '#4CAF50', borderRadius: '4px', transition: 'width 0.3s ease' }} /> </div> )} {/* 错误提示 */} {error && <p style={{ color: 'red' }}>{error}</p>} {/* 加载完成后展示列表 */} {!isLoading && !error && ( <ul style={{ listStyle: 'none', padding: 0 }}> {pokemonList.map(pokemon => ( <li key={pokemon.id} style={{ padding: '10px', borderBottom: '1px solid #eee' }}> <img src={pokemon.sprites.front_default} alt={pokemon.name} style={{ width: '50px', height: '50px', marginRight: '10px' }} /> <span style={{ textTransform: 'capitalize' }}>{pokemon.name}</span> </li> ))} </ul> )} </div> ); } export default App;
关键细节说明
useEffect依赖项:这里依赖isLoading,确保只有在加载状态为true时才执行请求,避免重复触发。- 进度更新优化:用
completed变量跟踪完成数,每次完成请求后更新进度,函数式更新也可以写成setProgress(prev => Math.round(((prev / 100) * total + 1) / total * 100)),效果一致。 - 请求取消:通过
CancelToken在组件卸载时取消未完成的请求,防止内存泄漏和控制台报错。 - 进度条动画:给进度条添加
transition属性,让进度变化更平滑。
内容的提问来源于stack exchange,提问作者Fraser Orr
相关产品推荐
相关产品推荐

