使用React钩子实现一个自增计数器

时间:2018-11-20 14:25:34

标签: javascript reactjs react-hooks

代码在这里: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>;
};

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

正确的方法是什么?在类组件中,使用持有间隔的实例变量很简单。

3 个答案:

答案 0 :(得分:10)

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

由于闭包是如何工作的,这将使counter变量始终引用初始值。您可以使用setCounter的函数版本来始终获取正确的值。

示例

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/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="root"></div>

答案 1 :(得分:1)

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

但是,您将需要更改setCounter以使用counter的先前值。原因是因为传递到setInterval的闭包中的回调仅访问第一个渲染中的counter变量,而无法访问后续渲染中的新counter值,因为useEffect()不会第二次被调用;在counter回调中,setInterval的值始终为0。

就像您熟悉的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/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 :(得分:0)

正如另一个答案已经显示的那样,可以使useEffect回调仅运行一次,并且与componentDidMount类似地工作。在这种情况下,由于功能范围带来的限制,必须使用状态更新程序功能,否则更新后的countersetInterval回调中将不可用。

另一种方法是在每次计数器更新上运行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>;
};