React useEffect触发异常:初始渲染重复加载25条API数据
Profile组件初始渲染重复加载数据,表格出现重复行
问题描述
Profile组件通过第三方API获取数据并在MatchTable展示,初始渲染时本该显示25行数据,却出现50行(第26-50行是前25行的重复)。点击"More..."按钮加载后续分页数据时正常,但初始加载重复。
调试时发现:
- 多次出现
unstable_flushDiscreteUpdates警告,推测带fetch的useEffect被触发两次且pageCounter值相同 - 两个react-hooks/exhaustive-deps警告:第一个useEffect缺少state.pageCounter依赖,第二个缺少state依赖
组件逻辑:pageCounter控制API分页(每次返回25条),初始加载第一页,点击按钮加载后续页(最多100条)。
代码片段
// import statements export const Profile = () => { const navigate = useNavigate(); const { id } = useParams(); const [state, setState] = useState( { arrays : { gameArray : [], categoryArray : [], mapArray : [], victoryArray : [], killsArray : [], deathsArray : [], }, counters : { totalKills : 0, totalDeaths : 0, totalVictories : 0, totalLosses : 0, totalKD : 0, totalWL : 0 }, pageCounter : 0} ); useEffect(() => { state.pageCounter++; }, []); useEffect(() => { const fetchHaloData = async () => { const url1 = 'https://cryptum.halodotapi.com/games/hmcc/stats/players/' ; const url2 = url1 + id; const url3 = `${url2}/recent-matches?page=` + state.pageCounter.toString(); // API provides 25 rows of data per call. const response = await fetch(url3, { "method": 'GET', "headers": { 'Content-Type': 'application/json', 'Cryptum-API-Version': '2.3-alpha', 'Authorization': 'MY_SECRET_TOKEN' } }); if (!response.ok) console.log("Response not okay!"); else { const res = await response.json(); let data = { gameArray : [], categoryArray : [], mapArray : [], victoryArray : [], killsArray : [], deathsArray : [], }; let stats = { totalKills : 0, totalDeaths : 0, totalVictories : 0, totalLosses : 0, totalKD : 0, totalWL : 0 }; if (state.arrays.gameArray[0] !== undefined) // data and stats need to be set to state initially to preserve the already fetched rows and stats calculations. { data = state.arrays; stats = state.counters; } for (let i=0; i< res.data.length; i++) { data.victoryArray.push(res.data[i].victory); data.victoryArray[data.victoryArray.length-1] ? stats.totalVictories++ : stats.totalLosses++ data.killsArray.push(res.data[i].stats.kills); stats.totalKills += data.killsArray[data.killsArray.length-1]; data.deathsArray.push(res.data[i].stats.deaths); stats.totalDeaths += data.deathsArray[data.deathsArray.length-1]; data.mapArray.push(res.data[i].details.map.name); data.categoryArray.push(res.data[i].details.category.name); data.gameArray.push(res.data[i].details.engine.name); } stats.totalKD = (stats.totalKills / stats.totalDeaths); stats.totalWL = (stats.totalVictories / stats.totalLosses); setState((prevState) => { return {...prevState, arrays : data, counters : stats}}); } } fetchHaloData().catch((error) => { console.log("There was an error:" + error)}) }, [id, state.pageCounter]); let more; if (state.pageCounter > 0 && state.pageCounter < 4) { if (state.arrays.gameArray.length % 25 === 0) // if the stats are less then 25 rows, there's no need to fetch more. more = <button id="More" onClick={ () => { setState((prevState) => { return {...prevState, pageCounter : prevState.pageCounter + 1}})}}>More...</button>; else more = null; } if (state.pageCounter >= 4) more = null; return ( <div> <NavBar /> <ButtonSearch /> <h3 id="search_result_counters">Showing last { state.arrays.gameArray.length } matches for "{ id }". Total K/D: { state.counters.totalKD }. Total W/L: { state.counters.totalWL }</h3> <MatchTable recentMatches={ state.arrays }/> { more } <CustomFooter position="stay_sticky" /> </div> ); }
问题根源与修复方案
1. 直接修改state导致初始加载异常
第一个useEffect里直接执行state.pageCounter++是错误操作——React state不能直接赋值,必须通过setState更新。这会导致state更新不生效,同时触发不必要的重复渲染,让第二个useEffect多次执行相同请求。
修复:
替换第一个useEffect,用函数式更新初始化pageCounter为1:
useEffect(() => { setState(prev => ({...prev, pageCounter: 1})); }, []);
2. useEffect依赖缺失与闭包问题
第二个useEffect依赖了state.pageCounter,但还隐式依赖整个state(比如state.arrays、state.counters),闭包会捕获旧的state值,导致重复追加相同数据;加上React Strict Mode下初始渲染的useEffect会被执行两次,进一步加剧重复加载问题。
修复:
- 拆分复杂state为多个独立的useState,减少不必要的依赖触发
- 使用函数式更新获取最新state,避免闭包问题
- 添加请求标识,避免组件卸载或分页变化时的竞态问题
优化后的核心代码:
// 拆分state,单独管理各模块,逻辑更清晰 const [arrays, setArrays] = useState({ gameArray: [], categoryArray: [], mapArray: [], victoryArray: [], killsArray: [], deathsArray: [], }); const [counters, setCounters] = useState({ totalKills: 0, totalDeaths: 0, totalVictories: 0, totalLosses: 0, totalKD: 0, totalWL: 0 }); const [pageCounter, setPageCounter] = useState(0); // 初始化分页计数器 useEffect(() => { setPageCounter(1); }, []); useEffect(() => { // 分页计数器为0时不执行请求 if (pageCounter === 0) return; let isCancelled = false; // 标记请求是否取消,避免竞态 const fetchHaloData = async () => { const url = `https://cryptum.halodotapi.com/games/hmcc/stats/players/${id}/recent-matches?page=${pageCounter}`; try { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Cryptum-API-Version': '2.3-alpha', 'Authorization': 'MY_SECRET_TOKEN' } }); if (!response.ok) throw new Error('Response not okay!'); const res = await response.json(); if (isCancelled) return; // 函数式更新,获取最新的arrays状态 setArrays(prev => { const newArrays = {...prev}; res.data.forEach(item => { newArrays.victoryArray.push(item.victory); newArrays.killsArray.push(item.stats.kills); newArrays.deathsArray.push(item.stats.deaths); newArrays.mapArray.push(item.details.map.name); newArrays.categoryArray.push(item.details.category.name); newArrays.gameArray.push(item.details.engine.name); }); return newArrays; }); // 函数式更新,获取最新的counters状态 setCounters(prev => { const newCounters = {...prev}; res.data.forEach(item => { item.victory ? newCounters.totalVictories++ : newCounters.totalLosses++; newCounters.totalKills += item.stats.kills; newCounters.totalDeaths += item.stats.deaths; }); // 处理除以0的情况,避免NaN newCounters.totalKD = newCounters.totalDeaths ? (newCounters.totalKills / newCounters.totalDeaths) : 0; newCounters.totalWL = newCounters.totalLosses ? (newCounters.totalVictories / newCounters.totalLosses) : 0; return newCounters; }); } catch (error) { console.log("There was an error:", error); } }; fetchHaloData(); // 清理函数,标记请求取消 return () => { isCancelled = true; }; }, [id, pageCounter]); // 依赖清晰,无缺失
3. 额外优化点
- 拆分state后,组件渲染更高效,只有对应state变化时才触发重渲染
- 添加除以0的判断逻辑,避免统计值出现NaN
- 请求取消逻辑防止分页切换时的旧请求覆盖新数据
内容的提问来源于stack exchange,提问作者GD1
相关产品推荐
相关产品推荐

