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

IT技术 javascript reactjs settimeout react-hooks
2021-02-04 06:13:03

我不明白为什么当我使用setTimeout函数时,我的react组件开始到无限的 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
}

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

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

每次运行时(除了在组件安装时首次渲染)和组件卸载(如果您不再显示组件)时,return () => { /*code/* }内部定义的函数useEffect都会useEffect运行。

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

沙盒示例

import { useState, useEffect } from "react";

const delay = 5;

export default function App() {
  const [show, setShow] = useState(false);

  useEffect(
    () => {
      let timer1 = setTimeout(() => setShow(true), delay * 1000);

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

  return show ? (
    <div>show is true, {delay}seconds passed</div>
  ) : (
    <div>show is false, wait {delay}seconds</div>
  );
}

如果您需要清除另一个组件中的超时或间隔:

沙盒示例。

import { useState, useEffect, useRef } from "react";

const delay = 1;

export default function App() {
  const [counter, setCounter] = useState(0);
  const timer = useRef(null); // we can save timer in useRef and pass it to child

  useEffect(() => {
    // useRef value stored in .current property
    timer.current = setInterval(() => setCounter((v) => v + 1), delay * 1000);

    // clear on component unmount
    return () => {
      clearInterval(timer.current);
    };
  }, []);

  return (
    <div>
      <div>Interval is working, counter is: {counter}</div>
      <Child counter={counter} currentTimer={timer.current} />
    </div>
  );
}

function Child({ counter, currentTimer }) {
  // this will clearInterval in parent component after counter gets to 5
  useEffect(() => {
    if (counter < 5) return;

    clearInterval(currentTimer);
  }, [counter, currentTimer]);

  return null;
}

丹·阿布拉莫夫的文章

如果您需要在“卸载时”和某些状态更改时重置计时器怎么办?你会设置两个钩子,一个是空数组,一个是相关的状态变量吗?
2021-03-14 06:13:03
@raRaRar return 在组件卸载时调用,你在说什么条件?
2021-03-27 06:13:03
没有竞争条件的风险吗?在尝试设置状态变量之前,我总是检查是否调用了 useEffect 中的 return 以防万一。
2021-03-28 06:13:03
这很有帮助,正如 Dan Abramov 自己的这篇文章一样overreacted.io/making-setinterval-declarative-with-react-hooks 链接到stackoverflow.com/a/59274757/470749这里是 useInterval 的 TypeScript 版本:gist。 github.com/Danziger/...
2021-04-03 06:13:03
@loopmode 我认为您可以在状态更改的代码中添加 clearTimeout(timer1) ,但是您需要将 timer1 保存在 useState 变量中。
2021-04-04 06:13:03

问题是你在调用setTimeoutexternal useEffect,所以每次渲染组件时你都会设置一个新的超时时间,最终会再次被调用并改变状态,迫使组件再次重​​新渲染,这将设置一个新的超时时间, ...

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

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

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

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

无论如何,我鼓励您创建自己的useTimeout钩子,以便您可以通过使用setTimeout declaratively来干燥和简化代码,正如 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/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>

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

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

您还可以找到的声明版本setTimeoutsetIntervaluseTimeoutuseInterval以书面的typescript一些额外的钩https://www.npmjs.com/package/@swyg/corre

@loopmode 对你也一样。这☝️可能会回答您关于在某些道具更改时清除计时器的问题。
2021-03-20 06:13:03
@mystrdat 这个☝️ 可能会回答您关于如何在某些道具更改时清除计时器的问题。在这个例子中,只需使用这些道具来传递 adelaynullto useInterval如果您通过null,超时将为您清除。
2021-03-22 06:13:03

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

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

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/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>

当您需要经常对某些道具更改运行效果,但只运行一个活动计时器并在卸载时清除它时,您将如何处理清除超时?
2021-03-29 06:13:03
export const useTimeout = () => {
    const timeout = useRef();
    useEffect(
        () => () => {
            if (timeout.current) {
                clearTimeout(timeout.current);
                timeout.current = null;
            }
        },
        [],
    );
    return timeout;
};

您可以使用简单的钩子来共享超时逻辑。

const timeout = useTimeout();
timeout.current = setTimeout(your conditions) 

我写了一个react-hooks,不再需要处理超时。就像 React.useState() 一样工作:

新答案

const [showLoading, setShowLoading] = useTimeoutState(false)

// sets loading to true for 1000ms, then back to false
setShowLoading(true, { timeout: 1000})
export const useTimeoutState = <T>(
  defaultState: T
): [T, (action: SetStateAction<T>, opts?: { timeout: number }) => void] => {
  const [state, _setState] = useState<T>(defaultState);
  const [currentTimeoutId, setCurrentTimeoutId] = useState<
    NodeJS.Timeout | undefined
  >();

  const setState = useCallback(
    (action: SetStateAction<T>, opts?: { timeout: number }) => {
      if (currentTimeoutId != null) {
        clearTimeout(currentTimeoutId);
      }

      _setState(action);

      const id = setTimeout(() => _setState(defaultState), opts?.timeout);
      setCurrentTimeoutId(id);
    },
    [currentTimeoutId, defaultState]
  );
  return [state, setState];
};

旧答案

const [showLoading, setShowLoading] = useTimeoutState(false, {timeout: 5000})

// will set show loading after 5000ms
setShowLoading(true)
// overriding and timeouts after 1000ms
setShowLoading(true, { timeout: 1000})

设置多个状态将刷新超时,并在与上次setState设置相同的毫秒后超时

Vanilla js(未测试,typescript版本是):

import React from "react"

// sets itself automatically to default state after timeout MS. good for setting timeouted states for risky requests etc.
export const useTimeoutState = (defaultState, opts) => {
  const [state, _setState] = React.useState(defaultState)
  const [currentTimeoutId, setCurrentTimeoutId] = React.useState()

  const setState = React.useCallback(
    (newState: React.SetStateAction, setStateOpts) => {
      clearTimeout(currentTimeoutId) // removes old timeouts
      newState !== state && _setState(newState)
      if (newState === defaultState) return // if already default state, no need to set timeout to set state to default
      const id = setTimeout(
        () => _setState(defaultState),
        setStateOpts?.timeout || opts?.timeout
      ) 
      setCurrentTimeoutId(id)
    },
    [currentTimeoutId, state, opts, defaultState]
  )
  return [state, setState]
}

typescript:

import React from "react"
interface IUseTimeoutStateOptions {
  timeout?: number
}
// sets itself automatically to default state after timeout MS. good for setting timeouted states for risky requests etc.
export const useTimeoutState = <T>(defaultState: T, opts?: IUseTimeoutStateOptions) => {
  const [state, _setState] = React.useState<T>(defaultState)
  const [currentTimeoutId, setCurrentTimeoutId] = React.useState<number | undefined>()
  // todo: change any to React.setStateAction with T
  const setState = React.useCallback(
    (newState: React.SetStateAction<any>, setStateOpts?: { timeout?: number }) => {
      clearTimeout(currentTimeoutId) // removes old timeouts
      newState !== state && _setState(newState)
      if (newState === defaultState) return // if already default state, no need to set timeout to set state to default
      const id = setTimeout(
        () => _setState(defaultState),
        setStateOpts?.timeout || opts?.timeout
      ) as number
      setCurrentTimeoutId(id)
    },
    [currentTimeoutId, state, opts, defaultState]
  )
  return [state, setState] as [
    T,
    (newState: React.SetStateAction<T>, setStateOpts?: { timeout?: number }) => void
  ]
}```