【问题标题】:Use React hook to implement a self-increment counter [duplicate]使用 React 钩子实现自增计数器 [重复]
【发布时间】:2019-04-23 00:34:18
【问题描述】:

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

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

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    您可以将一个空数组作为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 中,并且仅在delay 发生变化时创建一个新的区间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>

    【讨论】:

    • 你真的需要用 useRef 把它保存在这里吗?你不能直接调用回调而不引用它吗?
    【解决方案2】:

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

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

    和你熟悉的setState一样,状态钩子有两种形式:一种是接受更新的状态,另一种是传入当前状态的回调形式。你应该使用第二种形式并阅读最新的setState 回调中的 state 值,以确保在递增之前拥有最新的 state 值。

    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>

    【讨论】:

      【解决方案3】:

      正如another answer by @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>;
      };
      

      【讨论】:

      • 这并不理想,因为您将在每次渲染时继续运行setTimeout,似乎没有必要。
      • 我看不出有什么问题。不存在性能问题。如果合理,递归 setTimeout 是 JS 中 setInterval 的常见替代品。在这种特殊情况下,状态更新器函数在 setInterval 中看起来很好,即对于简单的增量。如果涉及多个州,情况将大不相同。
      猜你喜欢
      • 2021-12-04
      • 2019-08-24
      • 2019-12-17
      • 1970-01-01
      • 2019-07-28
      • 2012-05-12
      • 2021-02-15
      • 2019-12-12
      • 2021-12-26
      相关资源
      最近更新 更多