状态不会在setInterval内部更新

时间:2019-09-16 16:28:07

标签: javascript reactjs setinterval react-hooks

我正在制作一个简单的进度栏,当我使用setInterval时,我注意到了挂钩状态的奇怪行为。这是我的示例代码:

const {useState} = React;

const Example = ({title}) => {
  const [count, setCount] = useState(0);
  const [countInterval, setCountInterval] = useState(0)
  
  let intervalID
 
  const handleCount = () => {
    setCount(count + 1)
    console.log(count)
  }  
    
  const progress = () => {
    intervalID = setInterval(() => {
		setCountInterval(countInterval => countInterval + 1)
		console.log(countInterval)
		if(countInterval > 100) { // this is never reached
      setCountInterval(0)
      clearInterval(intervalID)
		}
	},100)
  }
  
  const stopInterval = () => {
    clearInterval(intervalID)
  }
    
  return (
    <div>
      <p>{title}</p>
      <p>You clicked {count} times</p>
      <p>setInterval count { countInterval } times</p>
      <button onClick={handleCount}>
        Click me
      </button>
      <button onClick={progress}>
        Run interval
      </button>
      <button onClick={stopInterval}>
        Stop interval
      </button>
    </div>
  );
};

// Render it
ReactDOM.render(
  <Example title="Example using simple hook:" />,
  document.getElementById("app")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>

如果我通过handleCount设置状态,一切都会按预期进行,但是当我运行progress函数时, inside setInterval countInterval完全没有改变。无论如何,countInterval 状态已更改。

要解决这个问题,我在progress函数内部使用了变量,如下所示:

  const progress = () => {
    let internalValue = 0
    intervalID = setInterval(() => {
        setCountInterval(internalValue)
        internalValue++
        if(internalValue > 100) {
      setCountInterval(0)
      clearInterval(intervalID)
        }
    },100)
  }

那很好,但是我仍然想知道是否有更好的方法,在第一种情况下我做错了什么。

第二个问题是我无法清除progress函数外部的间隔,并且不确定在这里做错了什么还是错过了什么?预先感谢您的任何帮助和建议。

2 个答案:

答案 0 :(得分:3)

您的问题是由于定时器参考在重新渲染时丢失而引起的,并且setInterval回调引用了setCountInterval的过期版本。

要使其完全正常运行,我建议添加一个状态变量以跟踪它是否已启动,并添加一个useEffect来处理setInterval的设置和清除。

const Example = ({ title }) => {
  const [count, setCount] = useState(0);
  const [countInterval, setCountInterval] = useState(0);
  const [started, setStarted] = useState(false);

  const handleCount = () => {
    setCount(count + 1);
    console.log(count);
  };

  useEffect(() => {
    let intervalID;
    if (started) {
      intervalID = setInterval(() => {
        setCountInterval(countInterval => countInterval + 1);
        console.log(countInterval);
        if (countInterval > 100) {
          setCountInterval(0);
          setStarted(false);
        }
      }, 100);
    } else {
      clearInterval(intervalID);
    }
    return () => clearInterval(intervalID);
  }, [started, countInterval]);

  return (
    <div>
      <p>{title}</p>
      <p>You clicked {count} times</p>
      <p>setInterval count {countInterval} times</p>
      <button onClick={handleCount}>Click me</button>
      <button onClick={() => setStarted(true)}>Run interval</button>
      <button onClick={() => setStarted(false)}>Stop interval</button>
    </div>
  );
};

此处的工作沙箱:https://codesandbox.io/s/elegant-leaf-h03mi

答案 1 :(得分:2)

countInterval是一个局部变量,包含一个原始值。根据JavaScript的性质,setState无法以任何方式使该变量变异。相反,它将重新执行整个Example函数,然后useState将返回新的更新值。这就是为什么在组件重新渲染之前您无法访问更新状态的原因。

将条件移到状态更新回调中可以轻松解决您的问题:

 setCountInterval(countInterval => countInterval > 100 ? 0 : countInterval + 1)

由于重新渲染,您不能使用局部变量,因此intervalId将在每次重新渲染时重新创建(并且其值会丢失)。使用useRef可以跨重用值。

  const interval = useRef(undefined);
  const [count, setCount] = useState(0);

  function stop() { 
    if(!interval.current) return;
    clearInterval(interval.current);
    interval.current = null;
  }

  function start() {
    if(!interval.current) interval.current = setInterval(() => {
      setCount(count => count > 100 ? 0 : count + 1);
    });
 }

 useEffect(() => {
   if(count >= 100) stop();
 }, [count]);