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

React hooks - right way to clear timeouts and intervals

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

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, {delay}seconds passed</div>
  ) : (
    <div>show is false, wait {delay}seconds</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.

您的计算机运行缓慢,因为您可能忘记将空数组作为 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>

问题是您在 useEffect 外部调用 setTimeout,因此您在每次渲染组件时设置新的超时,最终将再次调用并更改状态,强制组件再次重​​新渲染,这将设置一个新的超时,这...

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

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

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

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

无论如何,我鼓励您创建自己的 useTimeout 钩子,以便您可以通过使用 setTimeout declaratively, as Dan Abramov suggests for setInterval in Making setInterval Declarative with React Hooks 来 DRY 和简化您的代码,这非常相似:

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 和 return 超时 ID 来自动清除超时,以防您想自己手动取消它 ( Dan 的帖子中没有涉及这些内容)。

如果您正在寻找 setInterval 而不是 setTimeout 的类似答案,请查看:.

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

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

如果你想制作一个像“开始”这样的按钮,那么使用“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
}

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

useEffect(() => {
    let timeout;

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

    return () => {
      clearTimeout(timeout);
    };
  }, [yourDeps]);
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 组件中使用 setTimeout 在一段时间后执行一个函数或代码块。让我们探讨如何在 React 中使用 setTimeout。还有一个类似的方法叫做 setInterval

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

在间隔的情况下,通过使用 useEffect 钩子,在由其他人,您可能会受益于 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.

每 10 秒触发 api:

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

  return () => { 
    window.clearInterval(timer);
  }
}, [])

如果有任何状态变化:

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

  return () => { 
    window.clearInterval(timer);
  }
}, [state])