For a project I am working on, I had to make a call to my API every x
seconds to check for a certain value. Let’s say there can be two values returned as response - true
, or false
.
true
, nothing was to be done.false
, I had to show an alert and redirect the user to the homepage.Naturally, I decided to use setInterval
to run a function named myFunction()
every x seconds. myFunction
would make the call and if response was false
, navigate user to different screen. Now, when user was sent away to other screen, I wanted to stop the setInterval from running again. To stop the timer set by setInterval, you have to use clearInterval(id)
, where the id
is the value returned to you at the time when you called setInterval()
in the first place.
Since my setInterval
and clearInterval
calls were in different function scopes, I stored the ID returned from setInterval in the component’s state, and while clearing the interval, I thought I would use the ID stored in the state to clear the interval.
Before moving ahead, let’s see the code to understand the situation.
Note: I have to start the interval when the component is first rendered, so, I have used useEffect
with an empty dependency array.
// For storing the intervalID when we create itconst [intervalID, setIntervalID] = useState(0);
// For starting the interval ->useEffect(() => { let myIntervalID = setInterval(myFunction, 5000); setIntervalID(myIntervalID); }, []);
// The function that makes the callconst myFunction = () => { // make the call and get back the result if(result === false){ // Here, I want to clear the interval clearInterval(intervalID); // Navigate the user to other screen after the interval is cleared }}
Looks good, right. But, here’s the thing - the function passed to setInterval is defined once and it closes over the old stale value of state, which has not yet updated. So, the function passed to setInterval is created just one time when you call it. That means, while clearing the interval, it always considered the value of ID to be 0 (which was the initial state when we created the function passed to setInterval).
Well, then how will we clear our setInterval!
The actual “problem” here is that the function passed to setInterval is created just once at the start.
We know that the values inside any function in useEffect
are refreshed on every render, since useEffect
uses a new definition of the function you pass to it. So, each time, the function inside useEffect
“loses over a “fresh” value of the state.
Using this info, we can solve our problem! We can put the logic for clearing interval inside a useEffect
to make sure that it gets access to the fresh value of state when executed. One very important thing to note here is that you should never put unnecessary things in function passed to useEffect
, as it will run every time the values passed in dependency array are changed. Running things again and again unnecessarily will lead to bad performance.
In our case, the only thing that we absolutely need to put in useEffect
is the logic for clearing the interval. We’ll just put that and nothing else.
For this, we will use an extra state variable to decide whether to clear the setInterval timer. Let’s call it shouldIntervalBeCancelled
which will be initialized to false. Instead of clearing the interval in myFunction
, we will just set shouldIntervalBeCancelled
to be true there. Then, the actual clearing of interval will happen in a useEffect which has shouldIntervalBeCancelled
as the dependency.
Here’s the code for it -
const [intervalID, setIntervalID] = useState(0);const [shouldIntervalBeCancelled, setShouldIntervalBeCancelled] = useState(false);
// For starting the interval ->useEffect(() => { let myIntervalID = setInterval(myFunction, 5000); setIntervalID(myIntervalID); }, []);
useEffect(() => { if (shouldIntervalBeCancelled) { clearInterval(myIntervalID); // this being inside a useEffect makes sure that it gets the fresh value of state } }, [shouldIntervalBeCancelled]);
// The function that makes the callconst myFunction = () => { // make the call and get back the result if(result === "someValue"){ // Here, I want to clear the interval, I just set shouldIntervalBeCancelled to be true setShouldIntervalBeCancelled(true); // Navigate the user to other screen after the interval is cleared }}
This works, but it introduces extra state variables in our code. So the next logical step would be to extract this logic in a custom Hook and use that hook in our components whenever needed. This will ensure your code is clean and make it easy to debug if anything goes wrong in the future.
useInterval
by Dan AbramovHere, I have implemented my own solution which works for my use case. There are several other ways to solve this problem as well, for example, one can use useRef
to get the interval ID. If you want, you can go for this battle tested custom React Hook called “useInterval” by Dan Abramov which takes care of everything for you. I will also link his blog post regarding this in the references, do read it in case you want to dive deep in his approach for solving this problem :)
Hope this was helpful. Until next time!