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

Yan*_*Tay 55 javascript reactjs react-hooks

我正在尝试新的React Hooks并且有一个带有计数器的Clock组件,它应该每秒都会增加.但是,该值不会超过一个.

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'));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

Yan*_*Tay 76

原因是因为传递给setInterval闭包的回调只访问time第一个渲染中的变量,它不能访问time后续渲染中的新值,因为useEffect()第二次没有调用它.

timesetInterval回调中始终具有值0 .

就像setState你熟悉的那样,状态钩子有两种形式:一种是它处于更新状态,另一种是传递当前状态的回调形式.你应该使用第二种形式并在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'));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

  • 哈哈,我来这里插上我的帖子,它已经在答案了! (17认同)
  • 你很受欢迎,我花了一段时间盯着代码才发现我做了一个非常基本的(但可能是非显而易见的?)错误,并希望与社区分享 (5认同)
  • @YangshunTay 如果我只想读取 setInterval 内的状态值,我该怎么办? (3认同)
  • 这是非常有用的信息.谢谢杨顺! (2认同)
  • 在最好的情况下 (2认同)
  • @neosarchizo 你读过丹的帖子了吗?https://overreacted.io/making-setinterval-declarative-with-react-hooks/。如果您只想读取它,可以将更新后的值作为底部渲染的一部分读取。如果你想触发副作用,你可以添加一个`useEffect()`钩子并将该状态添加到依赖数组中。 (2认同)

Dan*_*ger 27

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

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

    return () => window.clearInterval(timer);
}, []);
Run Code Online (Sandbox Code Playgroud)

然后,每次setInterval滴答时,它实际上会调用setTime(time + 1),但time将始终保持setInterval定义回调(闭包)时的初始值。

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

setTime(prevTime => prevTime + 1);
Run Code Online (Sandbox Code Playgroud)

但我鼓励您创建自己的useInterval钩子,以便您可以通过setInterval 声明式使用来 DRY 和简化代码,正如 Dan Abramov 在使用 React Hooks 创建 setInterval Declarative 中所建议的那样:

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

    return () => window.clearInterval(timer);
}, []);
Run Code Online (Sandbox Code Playgroud)
setTime(prevTime => prevTime + 1);
Run Code Online (Sandbox Code Playgroud)
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'));
Run Code Online (Sandbox Code Playgroud)

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

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

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

您还可以在https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a 中找到setTimeoutand setIntervaluseTimeoutand 和用 TypeScript 编写useInterval的自定义useThrottledCallback钩子的声明版本。


Est*_*ask 11

useEffect 提供空输入列表时,仅在组件安装上评估一次该功能。

另一种方法setInterval是在setTimeout每次更新状态时设置新的时间间隔:

  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = setTimeout(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      clearTimeout(timer);
    };
  }, [time]);
Run Code Online (Sandbox Code Playgroud)

的性能影响setTimeout微不足道,通常可以忽略。除非组件是时间敏感到新设置的超时引起不希望的效应的点,都setIntervalsetTimeout方法是可以接受的。


Bea*_*oot 6

另一种解决方案是使用useReducer,因为它将始终传递当前状态。

function Clock() {
  const [time, dispatch] = React.useReducer((state = 0, action) => {
    if (action.type === 'add') return state + 1
    return state
  });
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      dispatch({ type: 'add' });
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

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

ReactDOM.render(<Clock />, document.querySelector('#app'));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


Esi*_*NER 5

useRef 可以解决这个问题,这里有一个类似的组件,每 1000ms 增加一次计数器

import { useState, useEffect, useRef } from "react";

export default function App() {
  const initalState = 0;
  const [count, setCount] = useState(initalState);
  const counterRef = useRef(initalState);

  useEffect(() => {
    counterRef.current = count;
  })

  useEffect(() => {
    setInterval(() => {
      setCount(counterRef.current + 1);
    }, 1000);
  }, []);

  return (
    <div className="App">
      <h1>The current count is:</h1>
      <h2>{count}</h2>
    </div>
  );
}
Run Code Online (Sandbox Code Playgroud)

我认为这篇文章将帮助你使用间隔来做反应钩子


pra*_*mar 5

const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((seconds) => {
        if (seconds === 5) {
          setSeconds(0);
          return clearInterval(interval);
        }
        return (seconds += 1);
      });
    }, 1000);
  }, []);
Run Code Online (Sandbox Code Playgroud)

注意:这将有助于使用 useState 挂钩更新和重置计数器。秒数将在 5 秒后停止。因为首先更改 setSecond 值,然后停止计时器并在 setInterval 内更新秒数。因为 useEffect 运行一次。