在 setInterval 中使用 React 状态挂钩时状态不更新

新手上路,请多包涵

我正在试用新的 React Hooks 并有一个带有计数器的时钟组件,该计数器应该每秒增加一次。但是,该值不会增加超过 1。

 function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
 <script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

原文由 Yangshun Tay 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 899
2 个回答

原因是因为回调传入 setInterval 的闭包仅访问 time 第一次渲染中的变量,它无法访问新的 time 后续渲染中的值,因为 useEffect() 没有被第二次调用。

timesetInterval 回调中始终具有 0 值。

就像你熟悉的 setState 一样,state hooks有两种形式:一种是获取更新状态,一种是传入当前状态的回调形式。你应该使用第二种形式并阅读 setState 回调中的最新状态值,以确保您在递增状态值之前拥有最新状态值。

奖励:替代方法

Dan Abramov 在他的 博客文章 中深入探讨了关于使用 setInterval 和钩子的主题,并提供了解决此问题的替代方法。强烈推荐阅读!

 function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(prevTime => prevTime + 1); // <-- Change this line!
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
 <script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

原文由 Yangshun Tay 发布,翻译遵循 CC BY-SA 4.0 许可协议

正如其他人指出的那样,问题在于 useState 仅被调用一次(如 deps = [] )以设置间隔:

 React.useEffect(() => {
    const timer = window.setInterval(() => {
        setTime(time + 1);
    }, 1000);

    return () => window.clearInterval(timer);
}, []);

Then, every time setInterval ticks, it will actually call setTime(time + 1) , but time will always hold the value it had initially when the setInterval 回调(闭包)被定义。

您可以使用 useState 的设置器的替代形式,并提供回调而不是您要设置的实际值(就像 setState 一样):

 setTime(prevTime => prevTime + 1);

但我鼓励您创建自己的 useInterval 挂钩,以便您可以通过使用 setInterval 声明 式地干燥和简化代码,正如 Dan Abramov 在 使用 React Hooks 进行 setInterval 声明 中所建议的:

 function useInterval(callback, delay) {
  const intervalRef = React.useRef();
  const callbackRef = React.useRef(callback);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setInterval ticks again, it
  // will still call your old callback.
  //
  // If you add `callback` to useEffect's deps, it will work fine but the
  // interval will be reset.

  React.useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Set up the interval:

  React.useEffect(() => {
    if (typeof delay === 'number') {
      intervalRef.current = window.setInterval(() => callbackRef.current(), delay);

      // Clear interval if the components is unmounted or the delay changes:
      return () => window.clearInterval(intervalRef.current);
    }
  }, [delay]);

  // Returns a ref to the interval ID in case you want to clear it manually:
  return intervalRef;
}

const Clock = () => {
  const [time, setTime] = React.useState(0);
  const [isPaused, setPaused] = React.useState(false);

  const intervalRef = useInterval(() => {
    if (time < 10) {
      setTime(time + 1);
    } else {
      window.clearInterval(intervalRef.current);
    }
  }, isPaused ? null : 1000);

  return (<React.Fragment>
    <button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
        { isPaused ? 'RESUME ⏳' : 'PAUSE 🚧' }
    </button>

    <p>{ time.toString().padStart(2, '0') }/10 sec.</p>
    <p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
  </React.Fragment>);
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
 body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

p + p {
  margin-top: 8px;
}

#app {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
}

button {
  margin: 32px 0;
  padding: 8px;
  border: 2px solid black;
  background: transparent;
  cursor: pointer;
  border-radius: 2px;
}
 <script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

除了生成更简单和更清晰的代码之外,这还允许您通过简单地传递 delay = null 来自动暂停(和清除)间隔,并且还返回间隔 ID,以防您想自己手动取消它(不包括在内)在 Dan 的帖子中)。

实际上,这也可以改进,以便它在未暂停时不会重新启动 delay ,但我想对于大多数用例来说这已经足够了。

如果您正在寻找 setTimeout 而不是 setInterval 的类似答案,请查看: https ://stackoverflow.com/a/59274757/3723993。

You can also find declarative version of setTimeout and setInterval , useTimeout and useInterval , a few additional hooks written in TypeScript in https:/ /www.npmjs.com/package/@swyg/corre

原文由 Danziger 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题