40

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

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

4

3 回答 3

89

您可以给一个空数组作为第二个参数,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>

一种更通用的方法是创建一个新的自定义钩子,将函数存储在 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>

于 2018-11-20T14:35:41.237 回答
11

正如@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>;
};
于 2018-11-20T16:08:04.737 回答
11

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

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

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>

于 2018-11-20T16:31:55.440 回答