163

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

10 回答 10

277

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

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

沙盒示例

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;
}

丹·阿布拉莫夫的文章

于 2018-10-31T19:41:59.033 回答
46

The problem is you are calling setTimeout outside 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 timeout, which...

So, as you have already found out, the way to use setTimeout or setInterval with hooks is to wrap them in useEffect, like so:

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.

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

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! ✨&lt;/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>

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

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

You can also find declarative version of setTimeout and setInterval, useTimeout and useInterval, a few additional hooks written in TypeScript in https://www.npmjs.com/package/@swyg/corre.

于 2019-12-10T20:18:39.233 回答
10

您的计算机滞后是因为您可能忘记将空数组作为第二个参数传入,并且在回调useEffect中触发了 a 。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>

于 2018-11-12T08:01:26.827 回答
6
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) 
于 2021-08-15T07:14:36.370 回答
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(未测试,打字稿版本是):

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
  ]
}```
于 2020-11-24T09:05:24.220 回答
4

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

useEffect(() => {
    let timeout;

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

    return () => {
      clearTimeout(timeout);
    };
  }, [yourDeps]);
于 2021-07-27T09:14:10.000 回答
2

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

useEffect(() => {
  const timer = setTimeout(() => {
    console.log('This will run after 1 second!')
  }, 1000);
  return () => clearTimeout(timer);
}, []);
于 2021-11-22T15:37:47.380 回答
1

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

于 2021-02-25T20:57:41.723 回答
1
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 处停止计时器。

于 2020-11-28T03:42:13.127 回答
1

如果在其他人给出的示例中通过使用钩子来避免将方法连续附加(安装)和分离(卸载)setInterval到事件循环的间隔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.
于 2021-11-27T12:52:15.710 回答