将 useState 与 setInterval 内的 if 条件一起使用不起作用

Using useState with an if conditional inside of setInterval isn't working

import { useState, useEffect } from "react";
import "./styles.css";

export default function App() {
  const [progress, setProgress] = useState(0);

  let progressTimer;

  function handleTime() {
    if (progress <= 100) {
      console.log("Progress: " + progress);
      setProgress((prevState) => (prevState += 10));
    } else {
      console.log("greater");
      clearInterval(progressTimer);
    }
  }

  function handlePlay() {
    console.log("Timer start");
    progressTimer = setInterval(handleTime, 1000);
  }

  useEffect(() => {
    handlePlay();
  });

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      {progress}
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

Codesandbox Link

期望的结果:转到 100,每 1 秒数 10。超过 100 后,关闭计时器。

实际结果:它一直在上升,每1秒超过10。

setInterval 对于 React,在第一次渲染时调用时,将导致间隔回调在第一次渲染后有状态变量的陈旧关闭。

我会改用 setTimeout,这样每当回调运行时,它就会具有最新状态的范围。

const { useState, useEffect } = React;

function App() {
  const [progress, setProgress] = useState(0);
  function handleTime() {
    if (progress <= 100) {
      console.log("Progress: " + progress);
      setProgress((prevState) => (prevState += 10));
    } else {
      console.log("greater");
    }
  }

  useEffect(() => {
    const timerId = setTimeout(handleTime, 1000);
    return () => clearTimeout(timerId);
  });

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      {progress}
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}


ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>

问题

  1. 启动间隔的 useEffect 没有依赖数组,因此每次渲染组件时都会启动一个新的间隔。这就是导致跳得越来越大的原因。
  2. 每个渲染周期都会重新声明 progressTimer,因此无法清除它。
  3. 当传递给 setInterval 回调时,progress 状态的检查在回调范围内关闭。您只会查看初始状态值。换句话说,这是一个陈旧的外壳。
  4. 在功能状态更新中使用 prevState => (prevState += 10) 实际上会改变先前的状态。应避免所有状态突变。

一个解决方案

  1. useEffect 添加一个依赖数组,以便它在组件安装时运行一次。将 handlePlay 逻辑移动到效果回调中,以便在安装时没有外部依赖性。不要忘记 return 清除函数以在组件卸载时清除任何 运行 间隔。
  2. progressTimer 存储为 React 引用,使其成为稳定的引用。
  3. 解除状态更新与清除计时器副作用的混合。使用第二个 useEffect 挂钩检查当前 progress 值何时达到 100。
  4. 在状态更新器中只是return前一个状态加10,prevState => prevState + 10作为下一个状态值。

代码

function App() {
  const [progress, setProgress] = useState(0);

  const progressTimer = useRef();

  function handleTime() {
    setProgress((prevState) => prevState + 10);
  }

  useEffect(() => {
    console.log("Progress: " + progress);
    if (progress >= 100) clearInterval(progressTimer.current);
  }, [progress]);

  useEffect(() => {
    console.log("Timer start");
    progressTimer.current = setInterval(handleTime, 1000);

    return () => clearInterval(progressTimer.current);
  }, []);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      {progress}
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}