Problem Summary: setTimeout
's are not clearing on mobile devices when using React's useEffect
hook. They are, however, clearing on desktop.
Problem Reproduction: https://codepen.io/amliving/pen/QzmPYE.
NB: run on a mobile device to reproduce the problem.
My Question: Why does my solution (explained below) work?
Details:
I'm creating a custom hook to detect idleness. Let's call it useDetectIdle
. It dynamically adds and removes event listeners to window
from a set of events, which when triggered call a provided callback after a period of time, via setTimeout
.
Here is the list of events that will be dynamically added to and then removed from window
:
const EVENTS = [
"scroll",
"keydown",
"keypress",
"touchstart",
"touchmove",
"mousedown", /* removing 'mousedown' for mobile devices solves the problem */
];
Here's the useDetectIdle
hook. The import piece here is that this hook, when its calling component unmounts, should clear any existing timeout (and remove all event listeners):
const useDetectIdle = (inactivityTimeout, onIdle) => {
const timeoutRef = useRef(null);
const callbackRef = useRef(onIdle);
useEffect(() => {
callbackRef.current = onIdle;
});
const reset = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
const id = setTimeout(callbackRef.current, inactivityTimeout);
timeoutRef.current = id;
};
useEffect(() => {
reset();
const handleEvent = _.throttle(() => {
reset();
}, 1000);
EVENTS.forEach(event => window.addEventListener(event, handleEvent));
return () => {
EVENTS.forEach(event => window.removeEventListener(event, handleEvent));
timeoutRef.current && clearTimeout(timeoutRef.current);
};
}, []);
};
useDetectIdle
is called inside components like this:
const Example = () => {
useDetectIdle(5000, () => alert("first"));
return <div className="first">FIRST</div>;
};
On non-touchscreen devices, useDetectIdle
works perfectly. But on mobile devices (both iOS and Android), any existing timeout is not cleared when its calling component unmounts. I.e. the callback passed to setTimemout
still fires.
My Solution: Through some trial and error, I discovered that removing mousedown
from the list of events solves the problem. Does anyone know what's happening under the hood?