I have a screen ponent that has a getPosition()
function called every second by an interval.
If the stopRace()
function is called or if the user presses the physical/graphical back button, I want to clear this interval so it doesn't continue to run in the background.
To do this, I've tried to store the interval ID in the raceUpdateInterval
state variable.
I then clear this interval using clearInterval(raceUpdateInterval)
in the stopRace()
function and the cleanup()
function.
When I call the stopRace()
function, then press back, the interval is cleared. I know this because my console logs:
Still Running
Still Running
Still Running
Reached cleanup function
However, if I press the back button, the interval does not clear. Instead my console logs:
Still Running
Still Running
Still Running
Reached cleanup function
Still Running
Followed by a memory leak warning containing the following advice:
To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function
Which is exactly what I'm trying to do, but is not working for some reason beyond my prehension.
Here is the relevant code for the ponent:
const RaceScreen = ({route, navigation}) => {
const [raceUpdateInterval, setRaceUpdateInterval] = useState(0);
useEffect(function() {
return function cleanup() {
console.log('Reached cleanup function')
clearInterval(raceUpdateInterval)
}
}, []);
function getPosition(){
console.log("Still being called")
//get position
}
function startRace(){
setRaceUpdateInterval(setInterval(getPosition, 1000))
}
function stopRace(){
clearInterval(raceUpdateInterval)
}
Why does the stopRace()
function correctly clear the interval but the cleanup()
function doesn't?
I have a screen ponent that has a getPosition()
function called every second by an interval.
If the stopRace()
function is called or if the user presses the physical/graphical back button, I want to clear this interval so it doesn't continue to run in the background.
To do this, I've tried to store the interval ID in the raceUpdateInterval
state variable.
I then clear this interval using clearInterval(raceUpdateInterval)
in the stopRace()
function and the cleanup()
function.
When I call the stopRace()
function, then press back, the interval is cleared. I know this because my console logs:
Still Running
Still Running
Still Running
Reached cleanup function
However, if I press the back button, the interval does not clear. Instead my console logs:
Still Running
Still Running
Still Running
Reached cleanup function
Still Running
Followed by a memory leak warning containing the following advice:
To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function
Which is exactly what I'm trying to do, but is not working for some reason beyond my prehension.
Here is the relevant code for the ponent:
const RaceScreen = ({route, navigation}) => {
const [raceUpdateInterval, setRaceUpdateInterval] = useState(0);
useEffect(function() {
return function cleanup() {
console.log('Reached cleanup function')
clearInterval(raceUpdateInterval)
}
}, []);
function getPosition(){
console.log("Still being called")
//get position
}
function startRace(){
setRaceUpdateInterval(setInterval(getPosition, 1000))
}
function stopRace(){
clearInterval(raceUpdateInterval)
}
Why does the stopRace()
function correctly clear the interval but the cleanup()
function doesn't?
3 Answers
Reset to default 7Part of the reason your code might not have been working as it was, is that if you ran the startRace
function more than once without stopping it in between, the interval would start up again but the interval ID would've been lost.
The main reason it failed to clear is that the raceUpdateInterval that it saw at the beginning when the useEffect with [] as a dependency array saw was: 0
. The reason why it didn't see the updated values is because useEffect creates a closure over the values at the point in which it runs (and re-runs). So you'd need to use a reference to give it access to the latest version of the raceUpdateInterval
Here's how I would modify your code to get it working properly. Instead of starting the timer in a function, use the useEffect
to start up that side effect that way there will never be an instance where the timer fails to clean up.
I added the function to the interval using a ref because I don't know how many closure variables there are in the getPosition function. This way the positionFunctRef.current always points to the latest version of the function rather than remaining static.
const RaceScreen = ({ route, navigation }) => {
const [runningTimer, setRunningTimer] = useState(false);
function getPosition() {
console.log('Still being called');
//get position
}
const positionFunctRef = useRef(getPosition);
useEffect(() => {
positionFunctRef.current = positionFunctRef;
});
useEffect(
function () {
if (!runningTimer) {
return;
}
const intervalId = setInterval(() => {
positionFunctRef.current();
}, 1000);
return () => {
console.log('Reached cleanup function');
clearInterval(intervalId);
};
},
[runningTimer]
);
function startRace() {
setRunningTimer(true);
}
function stopRace() {
setRunningTimer(false);
}
};
ponentWillUnmount
is use for cleanup (like removing event listeners, cancel the timer etc). Say you are adding a event listener in ponentDidMount
and removing it in ponentWillUnmount
as below.
ponentDidMount() {
window.addEventListener('mousemove', () => {})
}
ponentWillUnmount() {
window.removeEventListener('mousemove', () => {})
}
Hook equivalent of above code will be as follows
useEffect(() => {
window.addEventListener('mousemove', () => {});
// returned function will be called on ponent unmount
return () => {
window.removeEventListener('mousemove', () => {})
}
}, [])
So your better code is:
useEffect(function() {
setRaceUpdateInterval(setInterval(getPosition, 1000))
return function cleanup() {
console.log('Reached cleanup function')
clearInterval(raceUpdateInterval)
}
}, []);
Don't store something like an interval id in state, as re-renders occur each update. If you're functional, implement the setInterval
with useRef()
, if class based, use this.interval
.
Another gotcha is calling clearInterval()
in a functional ponent on the ref, instead of .current
heres a snippet from what i just debugged:
const spinnerCount = useRef(0)
const interval = useRef(null)
useEffect(() => {
if (withProgress && inProgress && notification == '') {
interval.current = setInterval(() => {
if (spinnerCount.current >= 40) {
clearInterval(interval.current)
spinnerCount.current = 0
setNotification('Something happened... Please try again.')
} else {
spinnerCount.current = spinnerCount.current + 1
}
}, 1000)
}
if (notification !== '' && inProgress === false) {
const delay = notification.length > 100 ? 6000 : 3000
setTimeout(() => {
clearInterval(interval.current)
spinnerCount.current = 0
setNotification('');
}, delay);
}
}, [inProgress])
Theres a little extra in there, basically this is a disappearing notification ponent, that also features a progress spinner. in this case, if the ponent is displaying the spinner, but a success/error notification is never triggered, the spinner will auto-quit after 40 seconds. hence the interval/spinnerCount