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

React 18严格模式下useEffect触发两次致数值重复递增的解决方法

解决React严格模式下useEffect触发两次导致状态重复递增的问题

问题背景

开发小型业余项目时,遇到React严格模式相关问题:每次useEffect触发时,状态变更会执行两次,导致资源数值被重复递增。以下是相关代码:

Context Provider中的代码

const updateBuilding = (building: ProductionBuilding): ProductionBuilding => {
  let elapsedTime = building.elapsedTime + deltaTime;
  if(elapsedTime >= building.productionTime){
      console.log("here");
      addRawResource(RawResource.IRON, 1);
      elapsedTime = 0;
  }
  return {...building, elapsedTime};
};

useEffect(() => {
  if(productionBuildings.length > 0){
      setProductionBuildings((previousState) => {
          return previousState.map(updateBuilding);
      })
  }
}, [frameTime]);

资源更新相关代码

const [rawResources, setRawResources] = useState<RawResources>({
  [RawResource.WATER]: 0,
  [RawResource.IRON]: 0,
  [RawResource.COPPER]: 0,
  [RawResource.WOOD]: 0
});

const addRawResource = (rawResource: RawResource, amount: number) => {
  setRawResources((prevValue) => {
      return {...prevValue, [rawResource]: prevValue[rawResource] + amount};
  })
};

frameTime与requestAnimationFrame绑定,定期循环重渲染应用,deltaTime由frameTime计算得出。

问题原因

React严格模式在开发环境下会刻意同步重复执行以下操作,用来检测非预期的副作用:

  • useEffect的回调函数
  • 状态更新器(即setState接收的回调函数)

你的代码中,updateBuilding函数在状态更新器内部被调用,而这个函数里直接执行了addRawResource这个副作用操作。严格模式下的重复执行导致每次触发时,资源被额外多增加一次。

另外,状态更新器函数应该是纯函数(只根据输入计算输出,不产生副作用),你的写法违反了这个原则,这也是严格模式要提醒的问题。

修复方案

方案1:解耦状态更新与副作用操作

先批量计算需要更新的建筑状态和需要添加的资源总量,再分别执行状态更新和副作用操作,确保逻辑只执行一次:

const updateBuilding = (building: ProductionBuilding): { updatedBuilding: ProductionBuilding; didProduce: boolean } => {
  let elapsedTime = building.elapsedTime + deltaTime;
  const didProduce = elapsedTime >= building.productionTime;
  
  if (didProduce) {
    elapsedTime = 0;
  }
  
  return {
    updatedBuilding: {...building, elapsedTime},
    didProduce
  };
};

useEffect(() => {
  if (productionBuildings.length === 0) return;

  let ironCount = 0;
  const newBuildings = productionBuildings.map(building => {
    const { updatedBuilding, didProduce } = updateBuilding(building);
    if (didProduce) ironCount++;
    return updatedBuilding;
  });

  // 先更新建筑状态
  setProductionBuildings(newBuildings);
  // 再统一添加资源
  if (ironCount > 0) {
    addRawResource(RawResource.IRON, ironCount);
  }
}, [frameTime]);

方案2:使用useRef标记执行状态

利用useRef的特性(不会随渲染重置值),标记当前帧是否已经处理过逻辑,避免严格模式下的重复执行:

const isFrameProcessed = useRef(false);

useEffect(() => {
  if (productionBuildings.length === 0 || isFrameProcessed.current) return;

  // 标记为已处理,阻止严格模式下的第二次执行
  isFrameProcessed.current = true;

  setProductionBuildings(prevState => {
    return prevState.map(updateBuilding);
  });

  // 重置标记,确保下一帧正常执行
  isFrameProcessed.current = false;
}, [frameTime]);

注意事项

  • 方案1是更推荐的写法,符合React对纯状态更新器的要求,逻辑更清晰也更稳定。
  • 严格模式的重复执行只发生在开发环境,生产环境不会有这个行为,但修复代码可以让你的逻辑更健壮,避免依赖执行次数的隐含bug。

内容的提问来源于stack exchange,提问作者Marko Taht

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 21:53:18