[英]setTimeout not clearing with React useEffect hook on mobile devices
Problem Summary : setTimeout
's are not clearing on mobile devices when using React's useEffect
hook.问题摘要:当使用 React 的
useEffect
钩子时, setTimeout
在移动设备上没有被清除。 They are, however, clearing on desktop.但是,它们正在桌面上清除。
Problem Reproduction : https://codepen.io/amliving/pen/QzmPYE .问题复现: 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
.我们称之为
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
.它从一组事件中动态地向
window
添加和删除事件侦听器,这些事件在一段时间后通过setTimeout
触发时调用提供的回调。
Here is the list of events that will be dynamically added to and then removed from window
:以下是将动态添加到
window
然后从window
删除的事件列表:
const EVENTS = [
"scroll",
"keydown",
"keypress",
"touchstart",
"touchmove",
"mousedown", /* removing 'mousedown' for mobile devices solves the problem */
];
Here's the useDetectIdle
hook.这是
useDetectIdle
钩子。 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: useDetectIdle
在组件内部被调用,如下所示:
const Example = () => {
useDetectIdle(5000, () => alert("first"));
return <div className="first">FIRST</div>;
};
On non-touchscreen devices, useDetectIdle
works perfectly.在非触摸屏设备上,
useDetectIdle
工作得很好。 But on mobile devices (both iOS and Android), any existing timeout is not cleared when its calling component unmounts.但是在移动设备(iOS 和 Android)上,当调用组件卸载时,不会清除任何现有的超时。 Ie the callback passed to
setTimemout
still fires.即传递给
setTimemout
的回调仍然会触发。
My Solution : Through some trial and error, I discovered that removing mousedown
from the list of events solves the problem.我的解决方案:通过反复试验,我发现从事件列表中删除
mousedown
可以解决问题。 Does anyone know what's happening under the hood?有谁知道引擎盖下发生了什么?
Note: this doesn't answer "why your solution works", resp.注意:这不能回答“为什么您的解决方案有效”,resp。 why it seemed to help, but it points out 2 bugs in your code that I think are the real cause of the behavior.
为什么它似乎有帮助,但它指出了您的代码中的 2 个错误,我认为这些错误是导致该行为的真正原因。 (Ie your solution does not really work.)
(即您的解决方案实际上不起作用。)
You are handling _.throttle
insufficiently - imagine the following scenario:您对
_.throttle
的处理不够充分 - 想象一下以下场景:
reset()
, even though your component is already unmounted (and from there it will fire the idle callback after another inactivityTimeout
ms).reset()
,即使您的组件已经卸载(并且从那里它会在另一个inactivityTimeout
ms 之后触发空闲回调)。 Why the bug was prevalent on mobile was probably tied with what the user had to do to unmount the component on mobile vs desktop, the timing, and what events fired while doing it.该错误在移动设备上普遍存在的原因可能与用户在移动设备与桌面设备上卸载组件时必须执行的操作、时间以及执行此操作时触发的事件有关。
There is also the very tiny possibility that your component's DOM gets unmounted, and because React >= 17.x runs effect cleanup methods asynchronously , a timeout could fire just before your effect cleanup method.您的组件的 DOM 被卸载的可能性也很小,并且因为 React >= 17.x 异步运行效果清理方法,超时可能会在您的效果清理方法之前触发。 I doubt this would be consistently simulated but can be fixed too.
我怀疑这是否会持续模拟,但也可以修复。
You can fix both issues by switching both effects to useLayoutEffect
and introducing local variable unmounted
:您可以通过切换这两种效应来解决这两个问题
useLayoutEffect
并引入局部变量unmounted
:
const useDetectIdle = (inactivityTimeout, onIdle) => {
const timeoutRef = useRef(null);
const callbackRef = useRef();
useLayoutEffect(() => {
callbackRef.current = onIdle;
});
const reset = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
const id = setTimeout(callbackRef.current, inactivityTimeout);
timeoutRef.current = id;
};
useLayoutEffect(() => {
reset();
let unmounted = false;
const handleEvent = _.throttle(() => {
if (!unmounted) reset();
}, 1000);
EVENTS.forEach(event => window.addEventListener(event, handleEvent));
return () => {
unmounted = true;
EVENTS.forEach(event => window.removeEventListener(event, handleEvent));
timeoutRef.current && clearTimeout(timeoutRef.current);
};
}, []);
};
PS: Idle callback after mount fires after inactivityTimeout
ms, whereas subsequent callbacks after inactivityTimeout + 1000
ms. PS:挂载后空闲回调在
inactivityTimeout
毫秒后触发,而后续回调在inactivityTimeout + 1000
毫秒后触发。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.