React挂钩-清除计时器和间隔的正确方法

时间:2018-10-31 19:09:30

标签: javascript reactjs settimeout react-hooks

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

7 个答案:

答案 0 :(得分:19)

每次运行useEffect时,useEffect中的

Return 函数都会运行(第一次运行是在组件安装时除外)。考虑一下它,因为每次执行新的useEffect时,旧的都会被删除。

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

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

如果您需要清除超时或间隔时间不在某处:

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

     const timerToClearSomewhere = useRef(false) //now you can pass timer to another component

     useEffect(
        () => {
          timerToClearSomewhere.current = setInterval(() => setShowLoading(true), 1000)

          return () => {
            clearInterval(timerToClearSomewhere.current)
          }
        },
        []
      )

  //here we can imitate clear from somewhere else place
  useEffect(() => {
    setTimeout(() => clearInterval(timerToClearSomewhere.current), 15000)
  }, [])

 return showLoading && <div>I will be visible after ~1000ms</div>
}

答案 1 :(得分:4)

您的计算机处于滞后状态,因为您可能忘记了将空数组作为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>

答案 2 :(得分:1)

问题是您在setTimeout之外调用useEffect,因此每次呈现组件时都要设置一个新的超时,该超时最终将再次被调用并更改状态,从而迫使该组件再次重新渲染,这将设置一个新的时间,

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

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

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

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

无论如何,我鼓励您创建自己的useTimeout挂钩,以便您可以使用setTimeout declaratively进行干燥和简化代码,如Dan Abramov建议的{{1} } 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>自动清除超时,并返回超时ID,以防您要手动取消超时(Dan的帖子中未涉及)

如果您正在寻找delay = null而不是setInterval的类似答案,请查看以下内容:https://stackoverflow.com/a/59274004/3723993

您还可以找到setTimeoutsetTimeoutsetIntervaluseTimeout的声明性版本,以及在{{3 }}。

答案 3 :(得分:0)

我写了一个React Hook,不再需要处理超时。 就像React.useState()一样工作,但是会超时默认为初始值,在这种情况下为false:

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

您还可以在特定的setStates上覆盖此超时时间:

const [showLoading, setShowLoading] = useTimeoutState(false, {timeout: 5000}) // can also not pass any timeout here
setShowLoading(true, {timeout: 1000}) // timeouts after 1000ms instead of 5000ms

设置多个状态只会刷新该功能,并且它将在与上一个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
  ]
}```

答案 4 :(得分:0)

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。

答案 5 :(得分: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
}

答案 6 :(得分:0)

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

useEffect(() => {
    let timeout;

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

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