React hooks - right way to clear timeouts and intervals
Asked Answered
L

11

244

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. Some people saying that function in timeout changing my state and that rerender component, that sets new timer and so on. Now I need to understand how to clear it's right.

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
}

Clear in different version of code not helping to:

const [showLoading, setShowLoading] = useState(true)
  let timer1 = setTimeout(() => setShowLoading(true), 1000)
  useEffect(
    () => {
      return () => {
        clearTimeout(timer1)
      }
    },
    [showLoading]
  )
Langan answered 31/10, 2018 at 19:9 Comment(8)
Can you share the code of useState and setShowLoadingOttoottoman
@Think-Twice useState is a proposed update to ReactJS's APILurie
@MarkC. Thank you I didn't know about it as I am not working on react currently. I think OP has to use setTimeout than using setInterval for showing loaderOttoottoman
i was able to shorten my code.Langan
@RTWTMI try with setTimeout method instead of setInterval. because what happens in your code is that setInterval triggeres for every one second you doing setState every second which you are not suppose to do in react and that's why you get that errorOttoottoman
@Think-Twice same problem. Some guys sad that timer update state and this rerenders component and then again. Now i think what to do to prevent it :/Langan
clear the timer when component unmounts like clearTimer(timer1);Ottoottoman
@Think-Twice i did it like this, same problem: const [showLoading, setShowLoading] = useState(true) let timer1 = setTimeout(() => setShowLoading(true), 1000) useEffect( () => { return () => { clearTimeout(timer1) } }, [showLoading] )Langan
L
396

Defined return () => { /*code/* } function inside useEffect runs every time useEffect runs (except first render on component mount) and on component unmount (if you don't display component any more).

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

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

If you need to clear timeouts or intervals in another component:

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.

Langan answered 31/10, 2018 at 19:41 Comment(5)
What if you need to reset the timer both "on unmounting" and when some state changes? Would you set up two hooks, one with an empty array and one with the relevant state variable?Pleven
@Pleven i think you can just add clearTimeout(timer1) in code where state changes, but you will need then to save your timer1 in useState variable.Langan
Is there no risk of race condition? I always check if the return in useEffect has been called just in case before trying to set a state variable.Duckbill
@raRaRar return called on component unmount, what condition are you talking about?Langan
This was helpful, as was this post from Dan Abramov himself here overreacted.io/making-setinterval-declarative-with-react-hooks linked by https://mcmap.net/q/110735/-react-hooks-right-way-to-clear-timeouts-and-intervals And here is a TypeScript version of useInterval: gist.github.com/Danziger/…Ratcliffe
G
71

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! ✨</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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/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://mcmap.net/q/108577/-state-not-updating-when-using-react-state-hook-within-setinterval.

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.

Gnosticism answered 10/12, 2019 at 20:18 Comment(2)
@mystrdat This ☝️ might answer your question about how to clear the timer on some props change. In this example, simply use those props to either pass a delay or null to useInterval. If you pass null, the timeout will be cleared for you.Gnosticism
@Pleven Same for you. This ☝️ might answer your question regarding clearing the timer when some props change.Gnosticism
A
11

I wrote a react hook to never again have to deal with timeouts. works just like React.useState():

New answer

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

Old answer

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

// will set show loading after 5000ms
setShowLoading(true)
// overriding and timeouts after 1000ms
setShowLoading(true, { timeout: 1000})

Setting multiple states will refresh the timeout 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
  ]
}```
Accomplishment answered 24/11, 2020 at 9:5 Comment(1)
This is great, so straightforward. Thank you!Heribertoheringer
T
10

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
Tyrelltyrian answered 12/11, 2018 at 8:1 Comment(1)
How would you deal with clearing the timeout when you need to run the effect on some prop change often, but run only one active timer and clear it on unmount?Indestructible
P
9

If your timeout is in the "if construction" try this:

useEffect(() => {
    let timeout;

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

    return () => {
      clearTimeout(timeout);
    };
  }, [yourDeps]);
Posada answered 27/7, 2021 at 9:14 Comment(0)
C
9
export const useTimeout = () => {
    const timeout = useRef();
    useEffect(
        () => () => {
            if (timeout.current) {
                clearTimeout(timeout.current);
                timeout.current = null;
            }
        },
        [],
    );
    return timeout;
};

You can use simple hook to share timeout logic.

const timeout = useTimeout();
timeout.current = setTimeout(your conditions) 
Contracture answered 15/8, 2021 at 7:14 Comment(1)
This is very elegant and simple solution, thanks, However there is an important caveat: the condition check in line 5 cause this not to work in my case, by removing it and executing clearTimeout in any case, made it work.Ogdoad
S
9

Trigger api every 10 seconds:

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

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

if any state change:

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

  return () => { 
    window.clearInterval(timer);
  }
}, [state])
Standup answered 5/5, 2022 at 13:6 Comment(0)
W
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>
)
}

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

Walczak answered 28/11, 2020 at 3:42 Comment(1)
Have you thought of stopping the interval as you reach 0 ?Katti
M
1

If you want to make a button like "start" then using "useInterval" hook may not be suitable since react doesn't allow you call hooks other than at the top of component.

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
}

Malave answered 25/2, 2021 at 20:57 Comment(0)
K
1

In case of Intervals to avoid continual attaching (mounting) and detaching (un-mounting) the setInterval method to the event-loop by the use of useEffect hook in the examples given by others, you may instead benefit the use of useReducer.

Imagine a scenario where given seconds and minutes you shall count the time down... Below we got a reducer function that does the count-down logic.

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

Now all we have to do is dispatch the cycle action in every interval:

  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.
Katti answered 27/11, 2021 at 12:52 Comment(0)
O
-3

Here is a solution in case timeout should be called outside component mount and does not require a custom hook, instead all logic is in the same component:

import React, { useState, useRef, useEffect } from 'react';

const clickWithTimeout = () => {
const [enter, setEnter] = useState(true);
const handleLeaveTimeout = useRef();
useEffect(()=>()=>{
clearTimeout(handleLeaveTimeout.current) 
} ,[])

return <div
          onMouseLeave={ 
          handleLeaveTimeout = 
          setTimeout(()=>setEnter(false)
              ,1000) 
          }
           >
            
         { enter ? <span> mouse is on</span> : <span> mouse is out</span> }
       </div> 
 }
Ogdoad answered 28/5, 2023 at 10:38 Comment(2)
There are already (at least) two answers that use this approach, what does yours bring to the table?Sough
@JaredSmith Thanks for the comment. This answer combine two different approaches, one is using the useEffect with no dependencies to clear the timeout, the other is calling useRef directly from the main function element, and not using custom hook/ util function. i didn't saw here an answer that combines these two elements. this approach allow the timeout to be cleared anywhere not only in when it is invoked on component mounting.Ogdoad

© 2022 - 2024 — McMap. All rights reserved.