Javascript React hooks - 清除超时和间隔的正确方法

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/53090432/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 05:03:40  来源:igfitidea点击:

React hooks - right way to clear timeouts and intervals

javascriptreactjssettimeoutreact-hooks

提问by RTW

I don't understand why is when I use setTimeoutfunction my react component start to infinite console.log. Everything is working, but PC start to lag as hell. Some people saying that function in timeout changing my state and that rerender component, that sets new timer and so on. Now I need to understand how to clear it's right.

我不明白为什么当我使用setTimeout函数时,我的反应组件开始到无限的 console.log。一切正常,但 PC 开始滞后。有人说超时功能会改变我的状态和重新渲染组件,设置新的计时器等等。现在我需要了解如何清除它是正确的。

export default function Loading() {
  // if data fetching is slow, after 1 sec i will show some loading animation
  const [showLoading, setShowLoading] = useState(true)
  let timer1 = setTimeout(() => setShowLoading(true), 1000)

  console.log('this message will render  every second')
  return 1
}

Clear in different version of code not helping to:

在不同版本的代码中清除无助于:

const [showLoading, setShowLoading] = useState(true)
  let timer1 = setTimeout(() => setShowLoading(true), 1000)
  useEffect(
    () => {
      return () => {
        clearTimeout(timer1)
      }
    },
    [showLoading]
  )

回答by RTW

Returnfunction in useEffectruns every time useEffectruns (except first run on component mount). Think about it as every time there is new useEffectexecution, the old one get deleted.

useEffect每次运行时都返回运行中的函数useEffect(除了第一次在组件安装时运行)。想象一下,每次有新的useEffect执行时,旧的都会被删除。

This is a working way to use and clear timeouts or intervals:

这是使用和清除超时或间隔的有效方法:

export default function Loading() {   
     const [showLoading, setShowLoading] = useState(false)

     useEffect(
        () => {
          let timer1 = setTimeout(() => setShowLoading(true), 1000)

          // this will clear Timeout when component unmount like in willComponentUnmount
          return () => {
            clearTimeout(timer1)
          }
        },
        [] //useEffect will run only one time
           //if you pass a value to array, like this [data] than clearTimeout will run every time this value changes (useEffect re-run)
      )

 return showLoading && <div>I will be visible after ~1000ms</div>
}

If you need to clear timeouts or intervals somewhere outside:

如果您需要清除外面某处的超时或间隔:

export default function Loading() {   
     const [showLoading, setShowLoading] = useState(false)

     const timerToClearSomewhere = useRef(false) //now you can pass timer to another component

     useEffect(
        () => {
          timerToClearSomewhere.current = setInterval(() => setShowLoading(true), 50000)

          return () => {
            clearInterval(timerToClearSomewhere.current)
          }
        },
        []
      )

  //here we can imitate clear from somewhere else place
  useEffect(() => {
    setTimeout(() => clearInterval(timerToClearSomewhere.current), 1000)
  }, [])

 return showLoading ? <div>I will never be visible because interval was cleared</div> : <div>showLoading is false</div>
}

If you need to manage queue (change state inside timer/interval), look for my answer here.

如果您需要管理队列(在计时器/间隔内更改状态),请在此处查找我的答案。

回答by Danziger

The problem is you are calling setTimeoutoutside useEffect, so you are setting a new timeout every time the component is rendered, which will eventually be invoked again and change the state, forcing the component to re-render again, which will set a new time, which...

问题是您正在调用setTimeoutexternal useEffect,因此每次渲染组件时都会设置新的超时时间,最终会再次调用并更改状态,迫使组件再次重​​新渲染,这将设置新的时间,从而...

So, as you have already found out, the way to use setTimeoutor setIntervalwith hooks is to wrap them in useEffect, like so:

因此,正如您已经发现的,使用setTimeoutsetInterval使用钩子的方法是将它们包裹在 中useEffect,如下所示:

React.useEffect(() => {
    const timeoutID = window.setTimeout(() => {
        ...
    }, 1000);

    return () => window.clearTimeout(timeoutID );
}, []);

As deps = [], useEffect's callback will only be called once. Then, the callback you return will be called when the component is unmounted.

因为deps = [],useEffect的回调只会被调用一次。然后,您返回的回调将在组件卸载时调用。

Anyway, I would encourage you to create your own useTimeouthook so that you can DRY and simplify your code by using setTimeoutdeclaratively, as Dan Abramov suggests for setIntervalin Making setInterval Declarative with React Hooks, which is quite similar:

无论如何,我鼓励你创建你自己的useTimeout钩子,这样你就可以通过使用setTimeoutdeclaratively来干燥和简化你的代码,正如 Dan AbramovsetInterval使用 React Hooks 制作 setInterval Declarative 中建议的那样,这非常相似:

function useTimeout(callback, delay) {
  const timeoutRef = React.useRef();
  const callbackRef = React.useRef(callback);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setTimeout kicks in, it
  // will still call your old callback.
  //
  // If you add `callback` to useEffect's deps, it will work fine but the
  // timeout will be reset.

  React.useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Set up the timeout:

  React.useEffect(() => {
    if (typeof delay === 'number') {
      timeoutRef.current = window.setTimeout(() => callbackRef.current(), delay);

      // Clear timeout if the components is unmounted or the delay changes:
      return () => window.clearTimeout(timeoutRef.current);
    }
  }, [delay]);

  // In case you want to manually clear the timeout from the consuming component...:
  return timeoutRef;
}

const App = () => {
  const [isLoading, setLoading] = React.useState(true);
  const [showLoader, setShowLoader] = React.useState(false);
  
  // Simulate loading some data:
  const fakeNetworkRequest = React.useCallback(() => {
    setLoading(true);
    setShowLoader(false);
    
    // 50% of the time it will display the loder, and 50% of the time it won't:
    window.setTimeout(() => setLoading(false), Math.random() * 4000);
  }, []);
  
  // Initial data load:
  React.useEffect(fakeNetworkRequest, []);
        
  // After 2 second, we want to show a loader:
  useTimeout(() => setShowLoader(true), isLoading ? 2000 : null);

  return (<React.Fragment>
    <button onClick={ fakeNetworkRequest } disabled={ isLoading }>
      { isLoading ? 'LOADING... ' : 'LOAD MORE ' }
    </button>
    
    { isLoading && showLoader ? <div className="loader"><span className="loaderIcon"></span></div> : null }
    { isLoading ? null : <p>Loaded! ?</p> }
  </React.Fragment>);
}

ReactDOM.render(<App />, document.querySelector('#app'));
body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

#app {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
}

button {
  margin: 32px 0;
  padding: 8px;
  border: 2px solid black;
  background: transparent;
  cursor: pointer;
  border-radius: 2px;
}

.loader {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 128px;
  background: white;
}

.loaderIcon {
  animation: spin linear infinite .25s;
}

@keyframes spin {
  from { transform:rotate(0deg) }
  to { transform:rotate(360deg) }
}
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Apart from producing simpler and cleaner code, this allows you to automatically clear the timeout by passing delay = nulland also returns the timeout ID, in case you want to cancel it yourself manually (that's not covered in Dan's posts).

除了生成更简单和更清晰的代码之外,这还允许您通过传递自动清除超时delay = null并返回超时 ID,以防您想手动取消它(这在 Dan 的帖子中没有涉及)。

If you are looking for a similar answer for setIntervalrather than setTimeout, check this out: https://stackoverflow.com/a/59274004/3723993.

如果您正在为setInterval而不是寻找类似的答案setTimeout,请查看:https: //stackoverflow.com/a/59274004/3723993

You can also find declarative version of setTimeoutand setInterval, useTimeoutand useInterval, plus a custom useThrottledCallbackhook written in TypeScript in https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a.

您还可以在https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a 中找到setTimeoutand setIntervaluseTimeoutand 和用 TypeScript 编写useInterval的自定义useThrottledCallback钩子的声明版本。

回答by Yangshun Tay

Your computer was lagging because you probably forgot to pass in the empty array as the second argument of useEffectand was triggering a setStatewithin the callback. That causes an infinite loop because useEffectis triggered on renders.

您的计算机滞后是因为您可能忘记将空数组作为第二个参数传入useEffectsetState在回调中触发 a 。这会导致无限循环,因为useEffect在渲染时触发。

Here's a working way to set a timer on mount and clearing it on unmount:

这是在安装时设置计时器并在卸载时清除它的工作方法:

function App() {
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      console.log('1 second has passed');
    }, 1000);
    return () => { // Return callback to run on unmount.
      window.clearInterval(timer);
    };
  }, []); // Pass in empty array to run useEffect only on mount.

  return (
    <div>
      Timer Example
    </div>
  );
}

ReactDOM.render(
  <div>
    <App />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>