React hooks - right way to clear timeouts and intervals

后端 未结 6 725
感情败类
感情败类 2020-12-01 02:54

I don\'t understand why is when I use setTimeout function my react component start to infinite console.log. Everything is working, but PC start to lag as hell.

相关标签:
6条回答
  • 2020-12-01 03:01
    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>
    )
    }
    

    Make sure to import useState and useEffect. Also, add the logic to stop the timer at 0.

    0 讨论(0)
  • 2020-12-01 03:02

    Your computer was lagging because you probably forgot to pass in the empty array as the second argument of useEffect and was triggering a setState within the callback. That causes an infinite loop because useEffect is triggered on renders.

    Here's a working way to set a timer on mount and clearing it on unmount:

    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>

    0 讨论(0)
  • 2020-12-01 03:04

    Return function in useEffect runs every time useEffect runs (except first run on component mount). Think about it as every time there is new useEffect execution, the old one get deleted.

    This is a working way to use and clear timeouts or intervals:

    export default function Loading() {   
         const [showLoading, setShowLoading] = useState(false)
          
         useEffect(
            () => {
              let timer1 = setTimeout(() => setShowLoading(null), 1000)
        
              // this will clear Timeout when component unmount 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>
    }
    

    If you need to clear timeouts or intervals somewhere outside:

    export default function Loading() {   
         const [showLoading, setShowLoading] = useState(false)
          
         const timerToClearSomewhere = useRef(null) //now you can pass timer to another component
    
         useEffect(
            () => {
              timerToClearSomewhere.current = setInterval(() => setShowLoading(true), 50000)
        
              return () => {
                clearInterval(timerToClearSomewhere.current)
              }
            },
            []
          )
    
      //here we can imitate clear from somewhere else place
      useEffect(() => {
        setTimeout(() => clearInterval(timerToClearSomewhere.current), 1000)
      }, [])
    
     return showLoading ? <div>I will never be visible because interval was cleared</div> : <div>showLoading is false</div>
    }
    

    Article from Dan Abramov.

    0 讨论(0)
  • 2020-12-01 03:07

    I wrote a react hook to never again have to deal with timeouts. works just like React.useState(), but will timeout to default the initial value, in this case false:

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

    You can also override this timeout on specific 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
    

    Setting multiple states will just refresh the function and it will timeout after the same ms that the last setState set.

    Vanilla js (not tested, typescript version is):

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

    Typescript:

    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
      ]
    }```
    
    0 讨论(0)
  • 2020-12-01 03:20

    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...                                                                     
    0 讨论(0)
  • 2020-12-01 03:21

    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 time, 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...                                                                     
    0 讨论(0)
提交回复
热议问题