You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 16:24:19