在 setInterval 中使用 React 状态挂接时状态未更新

2022-08-30 01:08:56

我正在尝试新的 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>

答案 1

原因是因为传入 的闭包的回调只访问第一次呈现中的变量,它无权访问后续呈现中的新值,因为第二次调用没有调用。setIntervaltimetimeuseEffect()

time在回调中始终具有值 0。setInterval

与您熟悉的一样,状态挂接有两种形式:一种是采用更新状态的形式,另一种是传入当前状态的回调形式。您应该使用第二种形式并读取回调中的最新状态值,以确保在递增之前具有最新的状态值。setStatesetState

奖励:替代方法

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>

答案 2

正如其他人所指出的,问题在于只调用一次(as)来设置间隔:useStatedeps = []

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

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

然后,每次价格变动时,它实际上都会调用 ,但将始终保持定义回调(闭包)时最初具有的值。setIntervalsetTime(time + 1)timesetInterval

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

setTime(prevTime => prevTime + 1);

但是我鼓励你创建自己的钩子,这样你就可以通过使用声明性的方式来干燥和简化你的代码,正如Dan Abramov在Make setInterval Declarative with React Hooks中建议的那样:useIntervalsetInterval

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