javascript 使用 React hook 实现自增计数器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/53395147/
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-10-29 10:10:34  来源:igfitidea点击:

Use React hook to implement a self-increment counter

javascriptreactjsreact-hooks

提问by PeiSong

The code is here: https://codesandbox.io/s/nw4jym4n0

代码在这里:https: //codesandbox.io/s/nw4jym4n0

export default ({ name }: Props) => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter(counter + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  });

  return <h1>{counter}</h1>;
};

The problem is each setCountertrigger re-rendering so the interval got reset and re-created. This might looks fine since the state(counter) keeps incrementing, however it could freeze when combining with other hooks.

问题是每个setCounter触发器都重新渲染,因此间隔被重置并重新创建。这可能看起来不错,因为状态(计数器)不断增加,但是当与其他钩子结合时它可能会冻结。

What's the correct way to do this? In class component it's simple with a instance variable holding the interval.

这样做的正确方法是什么?在类组件中,使用一个实例变量来保存间隔很简单。

回答by Tholle

You want to give an empty array as second argument to useEffectso that the function is only run once after the initial render.

您想提供一个空数组作为第二个参数,useEffect以便该函数仅在初始渲染后运行一次。

Because of how closures work, this will make the countervariable always reference the initial value. You can use the function version of setCounterinstead to always get the correct value.

由于闭包的工作方式,这将使counter变量始终引用初始值。您可以使用setCounter代替的函数版本来始终获得正确的值。

Example

例子

const { useState, useEffect } = React;

function App() {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter(counter => counter + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return <h1>{counter}</h1>;
};

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

回答by Yangshun Tay

The correct way to do this would be to run the effect only once. Since you only need to run the effect once because during mounting, you can pass in an empty array as a second argument to achieve.

正确的方法是只运行一次效果。由于您只需要运行一次效果,因为在挂载期间,您可以传入一个空数组作为第二个参数来实现。

However, you will need to change setCounterto use the previous value of counter. The reason is because the callback passed into setInterval's closure only accesses the countervariable in the first render, it doesn't have access to the new countervalue in the subsequent render because the useEffect()is not invoked the second time; counteralways has the value of 0 within the setIntervalcallback.

但是,您需要更改setCounter以使用 的先前值counter。原因是因为传入setInterval的闭包的回调只访问counter第一次渲染中的变量,它无法访问counter后续渲染中的新值,因为useEffect()第二次没有调用;countersetInterval回调中始终具有 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回调中读取最新的状态值在增加它之前确保你有最新的状态值。

function Counter() {
  const [counter, setCounter] = React.useState(0);
  React.useEffect(() => {
    const timer = setInterval(() => {
      setCounter(prevCount => prevCount + 1); // <-- Change this line!
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []); // Pass in empty array to run effect only once!

  return (
    <div>Count: {counter}</div>
  );
}

ReactDOM.render(<Counter />, 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

As another answer already shows, it's possible to make it useEffectcallback run only once and work similarly to componentDidMount. In this case it's necessary to use state updater function due to the limitations imposed by function scopes, otherwise updated counterwon't be available inside setIntervalcallback.

正如另一个答案已经显示的那样,可以让它useEffect回调只运行一次并与componentDidMount. 在这种情况下,由于函数作用域的限制,有必要使用状态更新器函数,否则更新counter将无法在setInterval回调中使用。

The alternative is to make useEffectcallback run on each counter update. In this case setIntervalshould be replaced with setTimeout, and updates should be limited to counterupdates:

另一种方法是useEffect在每次计数器更新时运行回调。在这种情况下setInterval应替换为setTimeout,并且更新应仅限于counter更新:

export default ({ name }: Props) => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => {
      setCounter(counter + 1);
    }, 1000);

    return () => {
      clearTimeout(timeout);
    };
  }, [counter]);

  return <h1>{counter}</h1>;
};