React严格模式下useEffect嵌套set函数异常调用3次问题排查
React严格模式下定时器嵌套状态更新的重复调用问题排查与优化
问题背景
在React 18严格模式下,定时器触发的状态更新逻辑出现异常:嵌套在setSecondPassed更新器中的setRow函数每个周期被调用3次,导致rows状态额外累加(预期每秒+1,实际每秒+2)。虽严格模式会双重调用状态更新器以检测非幂等代码,但当前代码的行为超出预期,需排查原因并优化幂等性。
原因分析
- 严格模式的双重执行机制:React 18开发环境的严格模式会双重调用所有状态更新器函数(包括
setState的回调参数),目的是暴露非幂等代码。但你的代码中,setRow被嵌套在setSecondPassed的更新器内,导致两次setSecondPassed回调执行时,都会触发一次setRow。 - 嵌套更新的状态依赖问题:第一次执行
setSecondPassed回调时,setRow会将rows从n更新为n+1;第二次执行setSecondPassed回调时,setRow的回调会读取到已经更新后的rows值(n+1),进而更新为n+2,最终导致rows每秒累加2次。 - 日志的表象误解:日志中出现的3次
setRow调用,本质是严格模式双重执行setSecondPassed回调带来的连锁反应——第一次回调触发setRow更新状态,第二次回调触发的setRow基于新状态再次更新,最终产生额外的状态变更。
优化方案
方案1:消除冗余状态(推荐)
rows状态完全可以由secondsPassed推导而来,无需单独维护,从根源上避免状态不同步和非幂等问题:
function Timer() { const [secondsPassed, setSecondPassed] = useState(0); const thresold = 10; function onInterval() { console.log("onInterval timer update"); setSecondPassed(prev => prev + 1); } useEffect(() => { const intervalId = setInterval(onInterval, 1000); console.log(`timer setinterval: ${intervalId}`); return () => { console.log(`timer clearInterval: ${intervalId}`); clearInterval(intervalId); }; }, []); // 直接由secondsPassed推导rows,无需单独维护状态 const rows = secondsPassed; const fixedRows = useMemo(() => { console.log(`fixedRows: ${rows}`); return Array.from({ length: rows }).map((_, i) => ( <div key={i} className="box-x" style={{ width: `${thresold}px` }}></div> )); }, [rows, thresold]); return ( <> {fixedRows} <div className="box-x" style={{ width: secondsPassed % thresold }}></div> </> ); }
方案2:避免嵌套状态更新,保证幂等性
若需保留rows独立状态,将setRow从setSecondPassed的回调中移出,改为独立调用。状态更新器使用函数式写法(本身具备幂等性),严格模式下的双重调用不会影响最终状态:
function Timer() { const [secondsPassed, setSecondPassed] = useState(0); const [rows, setRow] = useState(0); const thresold = 10; function onInterval() { console.log("onInterval timer update"); // 分开调用状态更新,避免嵌套 setSecondPassed(prev => prev + 1); setRow(prev => prev + 1); } useEffect(() => { const intervalId = setInterval(onInterval, 1000); console.log(`timer setinterval: ${intervalId}`); return () => { console.log(`timer clearInterval: ${intervalId}`); clearInterval(intervalId); }; }, []); const fixedRows = useMemo(() => { console.log(`fixedRows: ${rows}`); return Array.from({ length: rows }).map((_, i) => ( <div key={i} className="box-x" style={{ width: `${thresold}px` }}></div> )); }, [rows, thresold]); return ( <> {fixedRows} <div className="box-x" style={{ width: secondsPassed % thresold }}></div> </> ); }
验证结果
优化后,每个定时器周期内:
- 方案1中
rows完全由secondsPassed推导,不存在额外更新; - 方案2中严格模式会双重调用状态更新器,但React会忽略第二次执行的结果,
rows和secondsPassed均每秒仅更新1次,符合预期。
内容的提问来源于stack exchange,提问作者Ferry To
相关产品推荐
相关产品推荐

