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

Posted

技术标签:

【中文标题】React hooks - 清除超时和间隔的正确方法【英文标题】:React hooks - right way to clear timeouts and intervals 【发布时间】:2019-04-05 00:53:31 【问题描述】:

我不明白为什么当我使用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

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

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

【问题讨论】:

能否分享useState和setShowLoading的代码 @Think-Twice useState 是 ReactJS 的 API 的 proposed update @MarkC。谢谢,我不知道,因为我目前没有做出反应。我认为 OP 必须使用 setTimeout 而不是使用 setInterval 来显示加载器 我能够缩短我的代码。 @RTWTMI 尝试使用 setTimeout 方法而不是 setInterval。因为在您的代码中发生的情况是 setInterval 每隔一秒触发一次您每秒钟执行一次 setState ,这是您不应该在反应中执行的,这就是您收到该错误的原因 【参考方案1】:

useEffect 中定义的return () => /*code/* 函数在每次运行useEffect 时运行(组件安装时的第一次渲染除外)和组件卸载时(如果您不再显示组件)。

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

Sandbox example.

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, delayseconds passed</div>
  ) : (
    <div>show is false, wait delayseconds</div>
  );

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

Sandbox example.

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;

Article from Dan Abramov.

【讨论】:

如果您需要在“卸载”和某些状态更改时重置计时器怎么办?你会设置两个钩子,一个是空数组,一个是相关的状态变量吗? @loopmode 我认为您可以在状态更改的代码中添加 clearTimeout(timer1) ,但是您需要将 timer1 保存在 useState 变量中。 没有竞争条件的风险吗?在尝试设置状态变量之前,我总是检查是否调用了 useEffect 中的返回值。 @raRaRar return 在组件卸载时调用,你在说什么条件? 这很有帮助,丹·阿布拉莫夫本人在overreacted.io/making-setinterval-declarative-with-react-hooks***.com/a/59274757/470749 链接的这篇文章也是有帮助的,这是 useInterval 的 TypeScript 版本:gist.github.com/Danziger/…【参考方案2】:

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

所以,正如您已经发现的那样,将setTimeoutsetInterval 与钩子一起使用的方法是将它们包装在useEffect 中,如下所示:

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

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

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

无论如何,我鼓励您创建自己的 useTimeout 钩子,以便您可以使用 setTimeout declaratively 干燥和简化代码,正如 Dan Abramov 在 Making setInterval Declarative with React Hooks 中建议的 setInterval 那样,即非常相似:

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://***.com/a/59274004/3723993。

您还可以在https://www.npmjs.com/package/@swyg/corre 中找到setTimeoutsetIntervaluseTimeoutuseInterval 的声明式版本,以及一些用TypeScript 编写的附加钩子。

【讨论】:

@mystrdat 这☝️可能会回答您关于如何清除某些道具更改的计时器的问题。在此示例中,只需使用这些道具将delaynull 传递给useInterval。如果您通过null,将为您清除超时。 @loopmode 你也一样。这☝️可能会回答您有关在某些道具更改时清除计时器的问题。【参考方案3】:

您的计算机出现了延迟,因为您可能忘记将空数组作为useEffect 的第二个参数传递,并且在回调中触发了setState。这会导致无限循环,因为在渲染时会触发 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>

【讨论】:

当您需要经常在某些道具更改上运行效果但只运行一个活动计时器并在卸载时将其清除时,您将如何处理清除超时?【参考方案4】:
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) 

【讨论】:

【参考方案5】:

我写了一个反应钩子,再也不用处理超时了。 就像 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]

打字稿:

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
  ]
```

【讨论】:

【参考方案6】:

如果您的超时在“if 构造”中,试试这个:

useEffect(() => 
    let timeout;

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

    return () => 
      clearTimeout(timeout);
    ;
  , [yourDeps]);

【讨论】:

【参考方案7】:

在您的 React 组件中使用 setTimeout 在一段时间后执行函数或代码块。让我们探索如何在 React 中使用 setTimeout。还有一个类似的方法叫setInterval

useEffect(() => 
  const timer = setTimeout(() => 
    console.log('This will run after 1 second!')
  , 1000);
  return () => clearTimeout(timer);
, []);

【讨论】:

清除代码答案【参考方案8】:
const[seconds, setSeconds] = useState(300);

function TimeOut() 
useEffect(() => 
    let interval = setInterval(() => 
        setSeconds(seconds => seconds -1);
    , 1000);

    return() => clearInterval(interval);
, [])

function reset() 
  setSeconds(300); 
 

return (
    <div>
        Count Down: seconds left
        <button className="button" onClick=reset>
           Reset
        </button>
    </div>
)

确保导入 useState 和 useEffect。另外,添加逻辑以在 0 处停止计时器。

【讨论】:

你有没有想过在到达0时停止间隔?【参考方案9】:

如果你想制作一个像“开始”这样的按钮,那么使用“useInterval”钩子可能不合适,因为 react 不允许你在组件顶部调用钩子。

export default function Loading() 
  // if data fetching is slow, after 1 sec i will show some loading animation
  const [showLoading, setShowLoading] = useState(true)
  const interval = useRef();

  useEffect(() => 
      interval.current = () => setShowLoading(true);
  , [showLoading]);

  // make a function like "Start"
  // const start = setInterval(interval.current(), 1000)

  setInterval(() => interval.current(), 1000);

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


【讨论】:

【参考方案10】:

如果在其他人给出的示例中使用 useEffect 钩子来避免将 setInterval 方法连续附加(安装)和分离(卸载)到事件循环,您可能会受益使用useReducer

想象一个场景,给定secondsminutes,你应该倒计时...... 下面我们有一个执行倒计时逻辑的reducer 函数。

const reducer = (state, action) => 
  switch (action.type) 
    case "cycle":
      if (state.seconds > 0) 
        return  ...state, seconds: state.seconds - 1 ;
      
      if (state.minutes > 0) 
        return  ...state, minutes: state.minutes - 1, seconds: 60 ;
      
    case "newState":
      return action.payload;
    default:
      throw new Error();
  

现在我们要做的就是在每个时间间隔发送cycle 操作:

  const [time, dispatch] = useReducer(reducer,  minutes: 0, seconds: 0 );
  const  minutes, seconds  = time;

  const interval = useRef(null);
  
  //Notice the [] provided, we are setting the interval only once (during mount) here.
  useEffect(() => 
    interval.current = setInterval(() => 
      dispatch( type: "cycle" );
    , 1000);
    // Just in case, clear interval on component un-mount, to be safe.
    return () => clearInterval(interval.current);
  , []);

  //Now as soon as the time in given two states is zero, remove the interval.
  useEffect(() => 
    if (!minutes && !seconds) 
      clearInterval(interval.current);
    
  , [minutes, seconds]);
  // We could have avoided the above state check too, providing the `clearInterval()`
  // inside our reducer function, but that would delay it until the next interval.

【讨论】:

以上是关于React hooks - 清除超时和间隔的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

清除 React Hooks 中未安装组件上的内存泄漏

条件渲染,使用超时 Invalid hook call React

如何在 React Native 应用程序中使用 React hook useEffect 为每 5 秒渲染设置间隔?

React Native - 无法清除超时

React Hook Form - 在不清除表单的情况下重置“isDirty”?

使用 react-select 和 react-hook-form 返回正确的值