使用React挂钩实现自增量计数器

Pei*_*ong 11 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>;
};
Run Code Online (Sandbox Code Playgroud)

问题是每次setCounter触发重新渲染,因此间隔重置并重新创建.这可能看起来很好,因为状态(计数器)保持递增,但是当与其他挂钩组合时它可能会冻结.

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

Tho*_*lle 17

您希望将一个空数组作为第二个参数,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')
);
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


Yan*_*Tay 9

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

但是,您需要更改setCounter以使用 的先前值counter。原因是因为传入setInterval的闭包的回调只访问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'));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


Est*_*ask 8

正如@YangshunTay 的另一个答案已经表明的那样,可以让它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>;
};
Run Code Online (Sandbox Code Playgroud)

  • 这并不理想,因为您将在每次渲染上继续运行“setTimeout”,并且似乎没有必要。 (4认同)
  • 我看不出这有什么问题。不存在性能问题。递归的 setTimeout 是 JS 中 setInterval 的常用替代方法。在这种特殊情况下,状态更新器函数在 setInterval 中看起来很好,即简单的增量。如果涉及多个州,情况将大不相同。 (2认同)