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
相关产品推荐
相关产品推荐

