移动设备上的 React useEffect 挂钩未清除 setTimeout

Posted

技术标签:

【中文标题】移动设备上的 React useEffect 挂钩未清除 setTimeout【英文标题】:setTimeout not clearing with React useEffect hook on mobile devices 【发布时间】:2019-06-01 06:23:19 【问题描述】:

问题总结:使用 React 的 useEffect 挂钩时,setTimeout 无法在移动设备上清除。但是,它们正在桌面上清除。

问题重现:https://codepen.io/amliving/pen/QzmPYE。

注意:在移动设备上运行以重现问题。

我的问题:为什么我的解决方案(如下所述)有效?

详情: 我正在创建一个自定义挂钩来检测空闲情况。我们称之为useDetectIdle。它从一组事件中动态添加和删除window 的事件侦听器,当触发事件时,在一段时间后通过setTimeout 调用提供的回调。

这里是动态添加到window然后从window中删除的事件列表:

const EVENTS = [
  "scroll",
  "keydown",
  "keypress",
  "touchstart",
  "touchmove",
  "mousedown", /* removing 'mousedown' for mobile devices solves the problem */
];

这是useDetectIdle 钩子。这里的重要部分是这个钩子,当它的调用组件卸载时,应该清除任何现有的超时(并删除所有事件监听器):

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 在这样的组件内部被调用:

const Example = () => 
  useDetectIdle(5000, () => alert("first"));
  return <div className="first">FIRST</div>;
;

在非触摸屏设备上,useDetectIdle 可以完美运行。但在移动设备(iosandroid)上,任何现有的超时在其调用组件卸载时都不会清除。 IE。传递给 setTimemout 的回调仍然会触发。

我的解决方案:经过反复试验,我发现从事件列表中删除 mousedown 可以解决问题。有谁知道幕后发生了什么?

【问题讨论】:

也许是 Chrome ***.com/questions/41181372/… 的故意更改,但我仍然很惊讶之前没有遇到这种情况 【参考方案1】:

注意:这不回答“为什么你的解决方案有效”,resp。为什么它似乎有帮助,但它指出了您的代码中的 2 个错误,我认为这些错误是导致该行为的真正原因。 (即您的解决方案并没有真正起作用。)

您对_.throttle 的处理不足 - 想象以下场景:

    您的带有钩子的组件已安装。 用户触发事件之一 - 调用了节流函数,即它只是在内部将超时设置为 1000 毫秒(将在 1000 毫秒结束时调用节流回调)。 在超时之前,卸载组件。侦听器可以很好地删除,但内部超时仍然存在,并且最终会触发您的 reset(),即使您的组件已经卸载(并且从那里它将在另一个 inactivityTimeout 毫秒后触发空闲回调)。

为什么该错误在移动设备上普遍存在可能与用户在移动设备和桌面设备上卸载组件时必须执行的操作、时间以及执行此操作时触发的事件有关。

您的组件的 DOM 被卸载的可能性也非常小,并且因为 React >= 17.x 运行 effect cleanup methods asynchronously,可能会在您的效果清理方法之前触发超时。我怀疑这会一直模拟,但也可以修复。

您可以通过将两个效果切换为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:在inactivityTimeoutms 之后触发挂载后的空闲回调,而在inactivityTimeout + 1000ms 之后触发后续回调。

【讨论】:

以上是关于移动设备上的 React useEffect 挂钩未清除 setTimeout的主要内容,如果未能解决你的问题,请参考以下文章

React 对象不是 useEffect 上的函数错误

无法对未安装的组件执行 React 状态更新(useEffect 反应挂钩)

为啥我的组件在使用 React 的 Context API 和 useEffect 挂钩时会渲染两次?

React - 访问在 useEffect 挂钩中设置的对象属性

如何在 React 中使用 useEffect 挂钩调用多个不同的 api

从 React 中的 useEffect 挂钩更改复选框选中状态?