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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/53024496/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 05:03:08  来源:igfitidea点击:

State not updating when using React state hook within setInterval

javascriptreactjsreact-hooks

提问by Yangshun Tay

I'm trying out the new React Hooksand have a Clock component with a counter which is supposed to increase every second. However, the value does not increase beyond one.

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

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

回答by Yangshun Tay

The reason is because the callback passed into setInterval's closure only accesses the timevariable in the first render, it doesn't have access to the new timevalue in the subsequent render because the useEffect()is not invoked the second time.

原因是因为传入setInterval的闭包的回调只访问time第一次渲染中的变量,它无法访问time后续渲染中的新值,因为useEffect()第二次没有调用 。

timealways has the value of 0 within the setIntervalcallback.

timesetInterval回调中的值始终为 0 。

Like the setStateyou are familiar with, state hooks have two forms: one where it takes in the updated state, and the callback form which the current state is passed in. You should use the second form and read the latest state value within the setStatecallback to ensure that you have the latest state value before incrementing it.

就像setState你所熟悉的那样,状态钩子有两种形式:一种是接收更新状态的形式,另一种是传入当前状态的回调形式。你应该使用第二种形式并读取setState回调中的最新状态值来在增加它之前确保你有最新的状态值。

Bonus: Alternative Approaches

Dan Abramov, goes in-depth into the topic about using setIntervalwith hooks in his blog postand provides alternative ways around this issue. Highly recommend reading it!

奖励:替代方法

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

回答by Estus Flask

useEffectfunction is evaluated only once on component mount when empty input list is provided.

useEffect当提供空输入列表时,函数仅在组件安装时评估一次。

An alternative to setIntervalis to set new interval with setTimeouteach time the state is updated:

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

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

The performance impact of setTimeoutis insignificant and can be generally ignored. Unless the component is time-sensitive to the point where newly set timeouts cause undesirable effects, both setIntervaland setTimeoutapproaches are acceptable.

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

回答by Bear-Foot

An alternative solution would be to use useReducer, as it will always be passed the current state.

另一种解决方案是使用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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

回答by Danziger

As others have pointed out, the problem is that useStateis only called once (as its deps = []to set up the interval:

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

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

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

Then, every time setIntervalticks, it will actually call setTime(time + 1), but timewill always hold the value it had initially when the setIntervalcallback (closure) was defined.

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

You can use the alternative form of useState's setter and provide a callback rather than the actual value you want to set (just like with setState):

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

setTime(prevTime => prevTime + 1);

But I would encourage you to create your own useIntervalhook so that you can DRY and simplify your code by using setIntervaldeclaratively, as Dan Abramov suggests here in Making setInterval Declarative with React Hooks:

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

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Apart from producing simpler and cleaner code, this allows you to pause (and clear) the interval automatically by passing delay = nulland also returns the interval ID, in case you want to cancel it yourself manually (that's not covered in Dan's posts).

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

Actually, this could also be improved so that it doesn't restart the delaywhen unpaused, but I guess for most uses cases this is good enough.

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

If you are looking for a similar answer for setTimeoutrather than setInterval, check this out: https://stackoverflow.com/a/59274757/3723993.

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

You can also find declarative version of setTimeoutand setInterval, useTimeoutand useInterval, plus a custom useThrottledCallbackhook written in TypeScript in https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a.

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

回答by Vidya

Do as below it works fine.

按照下面的操作它工作正常。

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

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

//call increment function
increment(count);

回答by sumail

Tell React re-render when time changed.opt out

当时间改变时告诉 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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

回答by Jhonattan Oliveira

This solutions dont work for me because i need to get the variable and do some stuff not just update it.

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

I get a workaround to get the updated value of the hook with a promise

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

Eg:

例如:

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

With this i can get the value inside the setInterval function like this

有了这个,我可以像这样在 setInterval 函数中获取值

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);

回答by RTW

If someone need to manage a queue

如果有人需要管理队列

Let's say for showing notifications with interval of 3 sec (first in, first out), with ability to push new messages at any time.

假设显示间隔为 3 秒(先进先出)的通知,并且能够随时推送新消息。

Codesandbox example.

代码沙盒示例。

import React, {useState, useRef, useEffect} from "react";
import ReactDOM from "react-dom";

import "./styles.css";

let x = 1 // for testing
const fadeTime = 3000 // 3 sec 

function App() {
  // our messages array in what we can push at any time
  const [queue, setQueue] = useState([]) 

  // our shiftTimer that will change every 3 sec if array have items
  const [shiftTimer, setShiftTimer] = useState(Date.now())

  // reference to timer
  const shiftTimerRef = useRef(null)

  // here we start timer if it was mot started yet
  useEffect(() => {
    if (shiftTimerRef.current === null && queue.length != 0) {
      startTimer()
    }
  }, [queue])

  // here we will shift first message out of array (as it was already seen)
  useEffect(() => {
    shiftTimerRef.current = null
    popupShift()
  }, [shiftTimer])

  function startTimer() {
    shiftTimerRef.current = setTimeout(() => {
      setShiftTimer(Date.now)
    }, fadeTime )
  }

  function startTimer() {
    shiftTimerRef.current = setTimeout(() => setShiftTimer(Date.now), fadeTime )
  }

  function popupPush(newPopup) {
    let newQueue = JSON.parse(JSON.stringify(queue))
    newQueue.push(newPopup)
    setQueue(newQueue)
  }

  function popupShift() {
    let newQueue = JSON.parse(JSON.stringify(queue))
    newQueue.shift()
    setQueue(newQueue)
  }

  return (
    <div>
      <button onClick={() => popupPush({ message: x++ })}>Push new message</button>
      <div>{JSON.stringify(queue)}</div>
    </div>
  )
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);