React onMouseEnter和onMouseLeave的Bug,未能一致地工作。

6
我遇到了React的onMouseEnteronMouseLeave问题,有时候事件没有在应该触发的时候被触发。
我正在使用onMouseEnteronMouseLeave来显示/隐藏一个React Portal Modal提示面板:

enter image description here

enter image description here

问题:
如果鼠标光标缓慢悬停在图像上,则 onMouseEnteronMouseLeave 正常工作,但是如果鼠标光标快速(水平)悬停在图像上,则代码会中断,并显示多个提示面板(因为 onMouseLeave 未能触发)
这里是展示问题的视频链接: https://www.veed.io/view/7669d6c4-6b18-4251-b3be-a3fde8a53d54?sharingWidget=true 这里是我提到的错误的 codesandbox 链接: https://codesandbox.io/s/epic-satoshi-rjk38e 感激任何帮助。

也许你应该像这个例子一样使用 EventListeners 和从 UseEffect 中的 cleanup 函数: 链接 - MB_
1
@MB_将其转换为使用addEventListener()useRef(),但似乎没有任何区别,当鼠标光标快速悬停在图像上时,onMouseLeave仍然无法触发。 - yeln
3个回答

4
“失去事件”的原因不是因为事件监听器没有触发。问题在于您将操作虚拟 DOM 的 React 代码与操作浏览器 DOM 的“传统”JS 代码混合使用。这两个 DOM 大多数时间都不同步,这会导致您看到的故障。
请尝试删除所有直接访问 DOM 的代码,并仅在 React 组件(即虚拟 DOM)上进行操作。
以下是一个简化的示例,演示如何以纯 React 样式实现工具提示:

https://codesandbox.io/s/musing-villani-c0xv24?file=/src/App.js


我明白了,感谢指出原因,但是如果不使用JavaScript的.querySelector(),我应该如何编写逻辑来隐藏/显示“工具提示面板”?当光标离开“对齐框”并进入“工具提示面板”时,onmouseout => [显示/保持工具提示面板],否则隐藏它(如果不在“工具提示面板”中)? - yeln
onMouseEnter将状态变量showTooltip设置为trueonMouseLeave将此变量设置为false;仅在showTooltip==true时呈现工具提示面板。两个事件侦听器附加到不同的元素上,因此当鼠标进入悬停在JustifiedBox中心上方的不可见div时,处理'mouseEnter'事件,而mouseLeave事件由包含JustifiedBoxAlbumDetailsPanel的div处理。我的codesandbox中的迷你应用程序是否显示所需的行为?如果是,则可能不需要查询选择器。 - Steffen Frank

1

正如Steffen Frank所指出的那样,使用DOM事件通常是有风险的...
我采用了另一种策略,在index.js中使用React的onMouseMovemain容器上。

const [hoveredClasses, setHoveredClasses] = useState([]);

const handleMouseMove = (e) => {
  var x = e.clientX;
  var y = e.clientY;

  let elementMouseIsOver =
    document.elementFromPoint(x, y)?.closest(".c-justified-box") ||
    document.elementFromPoint(x, y)?.closest(".tooltip");

  let classes = elementMouseIsOver?.classList
    ? Array.from(elementMouseIsOver?.classList)
    : [];
  console.log(
    "[ c-justified-box ] > Mouse Move ----------------------------->",
    x,
    y,
    elementMouseIsOver,
    classes
  );

  // check if we need to cal setState
  if (
    (classes.length === 0 && hoveredClasses.length !== 0) ||
    !classes.every((classItem) => hoveredClasses.includes(classItem))
  ) {
    setHoveredClasses(classes);
  }
};

并且
<main className={styles.main} onMouseMove={handleMouseMove}>

在鼠标移动时:

  • 我们获取鼠标下面的元素
  • 我们检查该元素的类是否与我们之前存储在状态中的类完全相同。

如果不是,则将状态设置为这些类。这会触发整个main组件的重新绘制,因此每个JustifiedBox也会重新绘制。

我们向它们传递hoveredClasses数组...所以在它们每个内部,都设置了一个布尔值来决定是否显示模态框。

const showFloatingDetailPanel = props.hoveredClasses.includes(
  props.new_Album.slug
);

正如您所看到的...我们使用了props.new_Album.slug,它已经被用作c-justified-boxdiv类。

className={"c-justified-box" + " " + props.new_Album.slug}

我们只需要将它作为props传递给AlbumDetailsPanel,并对模态框的主要div执行相同的操作。

JustifiedBox.js:

<AlbumDetailsPanel
  slug={props.new_Album.slug}
  t_ref={floating}
  //...
  

AlbumDetailPanel.js:

<div
  className={"tooltip" + " " + props.slug}

每当hoveredClasses不匹配时,我们就会调用setState...并且这个状态将被每个组件使用来决定是否显示它们各自关联的AlbumDetailsPanel。现在不可能有多个展示。
这简化了JustifiedBox.js中的所有内容,因为我们摆脱了以下几点:
- handleAlbumMouseEnter - handleAlbumMouseOut - handleTooltipMouseEnter - handleTooltipMouseOut - 这3个useEffect 更新后的CodeSandbox

1

好问题。我之前也在一个组件和全局状态管理的问题上遇到过类似的困难。

看了你的代码后,我认为你应该添加onFocusonBlur 事件。

示例:

<div
  onMouseOver={() => handleEnter()}
  onMouseOut={() => handleExit()}
>

变成

<div
  onMouseOver={() => handleEnter()}
  onFocus={() => handleEnter()}
  onMouseOut={() => handleExit()}
  onBlur={() => handleExit()}
>

这也解决了移动设备没有相同鼠标状态的问题。

希望这能帮到你,愉快编程!


1
这个不起作用。 - innocent

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接