使用 React hook 实现自增计数器

IT技术 javascript reactjs react-hooks
2021-05-18 01:03:39

代码在这里: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个回答

您可以提供一个空数组作为第二个参数,useEffect以便该函数仅在初始渲染后运行一次。由于闭包的工作方式,这将使counter变量始终引用初始值。然后您可以使用setCounter代替的函数版本来始终获得正确的值。

例子

一种更通用的方法是创建一个新的自定义钩子,将函数存储在 a 中,ref并且仅在delay应更改时才创建一个新的间隔就像 Dan Abramov 在他的伟大博客文章“使用 React Hooks 进行 setInterval 声明”中所做的那样

例子

const { useState, useEffect, useRef } = React;

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    let id = setInterval(() => {
      savedCallback.current();
    }, delay);
    return () => clearInterval(id);
  }, [delay]);
}

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

  useInterval(() => {
    setCounter(counter + 1);
  }, 1000);

  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>

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

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

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

另一种方法是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>;
};