React hooks - 清除计时器的正确方法

RTW*_*RTW 18 javascript settimeout reactjs react-hooks

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

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
}
Run Code Online (Sandbox Code Playgroud)

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

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

RTW*_*RTW 32

这是一种使用和清除超时的工作方式:

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

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

          // this will clear Timeout when component unmont 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>
}
Run Code Online (Sandbox Code Playgroud)

  • 如果您需要“在卸载时”和某些状态更改时重置计时器,该怎么办?您是否将设置两个挂钩,一个带有一个空数组,另一个带有相关的状态变量? (3认同)
  • @loopmode我想你可以在状态改变的代码中添加clearTimeout(timer1),但是你需要将timer1保存在useState变量中。 (2认同)
  • 这很有帮助,就像 Dan Abramov 本人在这里发表的这篇文章 https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 链接 /sf/answers/4149233021/ 这里是 useInterval 的 TypeScript 版本:https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a#file-interval-hook-ts (2认同)

Dan*_*ger 32

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

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

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

    return () => window.clearTimeout(timeoutID );
}, []);
Run Code Online (Sandbox Code Playgroud)

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

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

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

    return () => window.clearTimeout(timeoutID );
}, []);
Run Code Online (Sandbox Code Playgroud)
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'));
Run Code Online (Sandbox Code Playgroud)
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) }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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


Gab*_*son 11

我写了一个反应钩子,再也不用处理超时了。工作原理与 React.useState() 类似:

新答案

const [showLoading, setShowLoading] = useTimeoutState(false)

// sets loading to true for 1000ms, then back to false
setShowLoading(true, { timeout: 1000})
Run Code Online (Sandbox Code Playgroud)
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];
};
Run Code Online (Sandbox Code Playgroud)

旧答案

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

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

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

Vanilla js(未测试,打字稿版本是):

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]
}
Run Code Online (Sandbox Code Playgroud)

打字稿:

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
  ]
}```
Run Code Online (Sandbox Code Playgroud)


Adi*_*ler 9

如果您的超时位于“if 构造”中,请尝试以下操作:

useEffect(() => {
    let timeout;

    if (yourCondition) {
      timeout = setTimeout(() => {
        // your code
      }, 1000);
    } else {
      // your code
    }

    return () => {
      clearTimeout(timeout);
    };
  }, [yourDeps]);
Run Code Online (Sandbox Code Playgroud)


Ale*_*yan 9

export const useTimeout = () => {
    const timeout = useRef();
    useEffect(
        () => () => {
            if (timeout.current) {
                clearTimeout(timeout.current);
                timeout.current = null;
            }
        },
        [],
    );
    return timeout;
};
Run Code Online (Sandbox Code Playgroud)

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

const timeout = useTimeout();
timeout.current = setTimeout(your conditions) 
Run Code Online (Sandbox Code Playgroud)


Gau*_*n E 9

每10秒触发一次api:

useEffect(() => {
  const timer = window.setInterval(() => {
    // function of api call 
  }, 1000);

  return () => { 
    window.clearInterval(timer);
  }
}, [])
Run Code Online (Sandbox Code Playgroud)

如果任何状态发生变化:

useEffect(() => {
  // add condition to state if needed
  const timer = window.setInterval(() => {
    // function of api call 
  }, 1000);

  return () => { 
    window.clearInterval(timer);
  }
}, [state])
Run Code Online (Sandbox Code Playgroud)


Yan*_*Tay 8

您的计算机滞后是因为您可能忘记将空数组作为第二个参数传入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")
);
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)