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

React严格模式下useEffect嵌套set函数异常调用3次问题排查

React严格模式下定时器嵌套状态更新的重复调用问题排查与优化

问题背景

在React 18严格模式下,定时器触发的状态更新逻辑出现异常:嵌套在setSecondPassed更新器中的setRow函数每个周期被调用3次,导致rows状态额外累加(预期每秒+1,实际每秒+2)。虽严格模式会双重调用状态更新器以检测非幂等代码,但当前代码的行为超出预期,需排查原因并优化幂等性。

原因分析

  1. 严格模式的双重执行机制:React 18开发环境的严格模式会双重调用所有状态更新器函数(包括setState的回调参数),目的是暴露非幂等代码。但你的代码中,setRow被嵌套在setSecondPassed的更新器内,导致两次setSecondPassed回调执行时,都会触发一次setRow。
  2. 嵌套更新的状态依赖问题:第一次执行setSecondPassed回调时,setRow会将rows从n更新为n+1;第二次执行setSecondPassed回调时,setRow的回调会读取到已经更新后的rows值(n+1),进而更新为n+2,最终导致rows每秒累加2次。
  3. 日志的表象误解:日志中出现的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 22:19:51