使用React hook实现自增计数器

Use React hook to implement a self-increment counter

代码在这里: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 触发重新渲染,因此间隔被重置并重新创建。这可能看起来不错,因为状态(计数器)不断递增,但是当与其他挂钩结合时它可能会冻结。

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

你可以给一个空数组作为 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>

一种更通用的方法是创建一个新的自定义挂钩,将函数存储在 ref and only creates a new interval if the delay should change, like Dan Abramov does in his great blog post "Making setInterval Declarative with React Hooks".

例子

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>

正如 已经显示的那样,可以使其 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>;
};

执行此操作的正确方法是 运行 效果仅一次。因为你只需要运行效果一次,因为在挂载时,你可以传入一个空数组作为第二个参数来实现。

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

和你熟悉的setState一样,state hooks有两种形式:一种是接受更新后的状态,另一种是传入当前状态的回调形式。你应该使用第二种形式并在 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>