You need to enable JavaScript to run this app. 梳理React onMouseEnter等事件状态冲突 解决移动端首次点击无法展开导航子项问题
优惠活动
大模型
产品
解决方案
定价
更多

梳理React onMouseEnter等事件状态冲突 解决移动端首次点击无法展开导航子项问题

问题根因
  • 移动端浏览器存在触摸转鼠标的模拟机制:用户手指触摸屏幕时,会在真实click事件触发前,自动在当前触摸位置依次派发模拟的mouseover、mouseenter、mousemove事件,目的是兼容只实现了鼠标交互逻辑的老旧网页。
  • 现有逻辑存在时序冲突,最终状态不符合预期:
    1. 组件初始状态isActive为false
    2. 用户首次触摸时,浏览器先派发模拟的onMouseEnter事件,执行setIsActive(true)触发状态更新
    3. 紧接着真实click事件派发,但click回调的闭包捕获的isActive还是更新前的旧值false,执行setIsActive(!isActive)时,实际会把isActive设回false
    4. 两次状态更新叠加的最终结果是isActive保持false,子元素不会展开,形成“首次点击无反应、第二次点击才生效”的感知。
修正方案

核心思路是从根源上隔离移动端的模拟鼠标事件,避免无意义的状态干扰,同时修正状态更新的闭包问题,具体改动如下:

  • 增加触摸状态标记,在触摸事件触发时打标记,拦截后续100ms内派发的模拟鼠标事件,不让模拟事件修改组件状态
  • 所有状态更新改为函数式写法,直接基于最新的状态值做变更,避免闭包捕获旧值导致的逻辑错乱
  • 完全保留PC端的鼠标悬停交互逻辑,不影响原有桌面端的使用体验

修正后的完整组件代码:

const ListItem = ({ children, title = '' }) => {
  const [isActive, setIsActive] = useState(false)
  // 用ref存储触摸标记,避免频繁触发组件重渲染
  const isTouchingRef = useRef(false)

  useEffect(() => {
    console.log('isActive', isActive)
  }, [isActive])

  return (
    <li 
      className={`depth1${isActive ? ' is-active' : ''}`}
      onTouchStart={() => {
        // 触摸开始时打标记,100ms后清除(匹配浏览器模拟鼠标事件的常规派发延迟)
        isTouchingRef.current = true
        setTimeout(() => {
          isTouchingRef.current = false
        }, 100)
      }}
      onMouseEnter={() => {
        // 触摸触发的模拟鼠标事件直接跳过,不修改状态
        if (isTouchingRef.current) return
        console.log('onMouseEnter')
        setIsActive(true)
      }} 
      onMouseLeave={() => {
        if (isTouchingRef.current) return
        console.log('onMouseLeave')
        setIsActive(false)
      }}
    >
      <a 
        href="#" 
        onClick={(e) => {
          console.log('onClick a')
          e.preventDefault()
          // 用函数式更新,拿到最新的状态值做取反,彻底规避闭包旧值问题
          setIsActive(prev => !prev)
        }}
      >
        {title}
      </a>
      {isActive && (
        <ul 
          onClick={() => {
            console.log('onClick ul')
            setIsActive(prev => !prev)
          }}
        >
          {children}
        </ul>
      )}
    </li>
  )
}
方案效果
  • 移动端首次点击时,触摸触发的模拟onMouseEnter会被拦截,不会修改状态,真实click事件触发时直接把isActive从false更新为true,子元素立刻展开,不需要二次点击
  • PC端没有触摸事件触发的标记,原有的悬停展开、移出收起的逻辑完全不受影响,和原有体验一致
  • 100ms的拦截阈值匹配主流移动端浏览器的事件派发延迟,不会误判真实用户的鼠标操作

可选优化:可以通过window.matchMedia('(hover: hover)')判断当前设备是否支持悬停交互,对于完全不支持悬停的纯触摸设备,可直接跳过鼠标事件的状态修改逻辑,减少不必要的事件判断开销。


内容的提问来源于stack exchange,提问作者dev_el

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.28 13:54:01