useEffect 中的状态不更新

State within useEffect not updating

我目前正在用 ReactJS 构建一个计时器进行练习。目前我有两个组件,一个 Timer 组件,它显示时间并在安装时设置间隔。我还有一个 App 组件,它跟踪主要状态,例如计时器是否为 paused 的状态以及 timer.

的当前值

我的目标是当我单击 pause 按钮时,计时器停止递增。目前我试图通过使用来实现这一点:

if(!paused) 
  tick(time => time+1);

在我看来,当 pausedfalse 时,应该只增加 time 状态。但是,当我通过单击按钮更新 paused 状态时,setTimeout 中的 paused 状态不会改变。我猜 setTimeout 正在对 paused 状态形成一个闭包,所以当状态改变时它不会更新。我尝试将 paused 添加为 useEffect 的依赖项,但这会导致在 paused 更改时排队多次超时。

const {useState, useEffect} = React;

const Timer = ({time, paused, tick}) => {
  useEffect(() => {
    const timer = setInterval(() => {
      if(!paused) // `paused` doesn't change?
        tick(time => time+1);
    }, 1000);
  }, []);
  
  return <p>{time}s</p>
}

const App = () => {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  const togglePaused = () => setPaused(paused => !paused);
  
  return (
    <div>
      <Timer paused={paused} time={time} tick={setTime} />
      <button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.body);
<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>

那么,我的问题是:

  1. 为什么我当前的代码不起作用(为什么 paused 没有在 useEffect 内更新)?
  2. 有什么方法可以使上述代码正常工作,以便我可以“暂停”我在 useEffect() 内设置的时间间隔?

停止定时器return函数从useEffect().

清除间隔

React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.

(source: Using the Effect Hook - Effects with Cleanup)

您还应该在 dependencies 数组中传递 paused 以停止 useEffect 在每次重新渲染时创建新的间隔。如果你添加 [paused] 它只会在 paused 变化时创建新的间隔。

const {useState, useEffect} = React;

const Timer = ({time, paused, tick}) => {
  useEffect(() => {
    const timer = setInterval(() => {
      if(!paused) // `paused` doesn't change?
        tick(time => time+1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, [paused]);
 
  return <p>{time}s</p>
}

const App = () => {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  const togglePaused = () => setPaused(paused => !paused);
  
  return (
    <div>
      <Timer paused={paused} time={time} tick={setTime} />
      <button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<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="root"></div>

setInterval 第一次捕获 paused 值,您需要删除间隔,每次更改暂停时重新创建它。

您可以查看这篇文章了解更多信息:https://overreacted.io/making-setinterval-declarative-with-react-hooks

您可以使用 Dan 的 useInterval 钩子。我无法比他更好地解释它 why 你应该使用它。

钩子看起来像这样。

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

  // Remember the latest callback.
  React.useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  React.useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

然后像这样在您的 Timer 组件中使用它。

const Timer = ({ time, paused, tick }) => {
  useInterval(() => {
    if (!paused) tick(time => time + 1);
  }, 1000);

  return <p>{time}s</p>;
};

我认为不同之处在于 useInterval 钩子知道它的依赖关系(暂停),而 setInterval 不是。