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


107

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

Answers:


155

原因是因为传递给setInterval的闭包中的回调仅访问time第一个渲染器中的变量,而无法访问time后续渲染器中的新值,因为useEffect()第二次未调用。

timesetInterval回调中的值始终为0 。

就像setState您熟悉的状态钩子一样,状态钩子有两种形式:一种是处于更新状态的状态,另一种是用于传递当前状态的回调形式。您应该使用第二种形式,并读取setState回调中的最新状态值以在递增之前,请确保您具有最新的状态值。

奖励:替代方法

Dan AbramovsetInterval在他的博客文章中深入探讨了有关使用钩子的主题,并提供了解决此问题的替代方法。强烈建议阅读!

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>


3
这是非常有用的信息。谢谢阳顺!
Tholle

8
不客气,我花了一段时间盯着代码,才发现我犯了一个非常基本的(但可能不是显而易见的)错误,并想与社区分享这一点
Yangshun Tay

3
SO处于最佳状态
Patrick Hund

42
哈哈我来这是为了塞我的帖子,答案已经在这里了!
丹·阿布拉莫夫,

2
很棒的博客文章。睁大眼睛。
benjaminadk

19

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

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

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

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


12

正如其他人指出的那样,问题在于useState仅调用一次(因为它deps = []设置了间隔:

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

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

然后,每次setInterval打勾时,它实际上都会调用setTime(time + 1),但time始终保留当setInterval定义回调(关闭)。

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

setTime(prevTime => prevTime + 1);

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

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'));
body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

p + p {
  margin-top: 8px;
}

#app {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
}

button {
  margin: 32px 0;
  padding: 8px;
  border: 2px solid black;
  background: transparent;
  cursor: pointer;
  border-radius: 2px;
}
<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>

除了生成更简单,更清晰的代码外,这还允许您通过以下方式自动暂停(清除)时间间隔: delay = null并返回间隔ID,以防您想手动取消它(Dan的帖子中未介绍)。

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

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

您还可以找到的声明版本setTimeoutsetIntervaluseTimeoutuseInterval,以及自定义useThrottledCallback的写在打字稿钩https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a


5

另一种解决方案是使用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'));
<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>


为什么useEffect在依赖项数组为空的情况下,为什么要多次调用此处以更新时间,这意味着useEffect仅在组件/应用程序首次呈现时才调用?
BlackMath

1
@BlackMathuseEffect当组件确实第一次渲染时,内部函数仅被调用一次。但是在它的内部,有一个setInterval负责定期更改时间的工具。我建议您阅读一些有关的内容setInterval,那之后应该更清楚了!developer.mozilla.org/en-US/docs/Web/API/...
熊英尺

3

执行以下操作可以正常工作。

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

async function increment(count,value) {
    await setCount(count => count + 1);
  }

//call increment function
increment(count);

count => count + 1回调是什么工作适合我,谢谢!
Nickofthyme

0

当时间改变时,告诉React重新渲染。选择退出

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, [time]);

  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
问题在于,每次count更改后,计时器都会被清除并重置。
sumail

而且因为setTimeout()如Estus所指出的那样,它是首选
Chayim Friedman

0

这个解决方案对我不起作用,因为我需要获取变量并做一些事情,而不仅仅是更新它。

我有一个解决方法来获得带有承诺的钩子的更新值

例如:

async function getCurrentHookValue(setHookFunction) {
  return new Promise((resolve) => {
    setHookFunction(prev => {
      resolve(prev)
      return prev;
    })
  })
}

这样我可以像这样在setInterval函数中获取值

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.