You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React TypeScript:useRef监听外部触摸事件仅热更新后生效问题

问题分析与解决方案

你的问题核心是闭包陷阱导致自定义Hook中的activeMenu引用未实时更新,加上依赖数组不完整,使得事件回调始终使用初始渲染时的变量,只有热更新后才会重新生成回调获取最新值。同时还存在事件监听覆盖不全、直接操作DOM不符合React模式的问题,下面逐一解决:

1. 修复闭包与依赖问题

当前handleClickOutside捕获的是组件初始渲染时的activeMenu引用,后续组件重新渲染时该引用不会自动更新。且useEffect的依赖数组缺少styles['active'],导致回调不会随样式变化重新创建。

修改自定义Hook,改为在回调中实时获取菜单元素,避免依赖外部变量:

function useOutsideListener(ref: React.MutableRefObject<null | HTMLDivElement>) {
  useEffect(() => {
    function handleClickOutside(event: MouseEvent | TouchEvent) {
      if (ref.current && !ref.current.contains(event.target as Element)) {
        // 实时获取菜单元素,规避闭包陷阱
        const activeMenu = document.querySelectorAll('[data-menu]');
        activeMenu.forEach((item) => {
          item.classList.remove(styles['active']);
        });
      }
    }

    // 同时监听click和touchstart,覆盖桌面与移动端场景
    document.addEventListener('click', handleClickOutside);
    document.addEventListener('touchstart', handleClickOutside);

    return () => {
      document.removeEventListener('click', handleClickOutside);
      document.removeEventListener('touchstart', handleClickOutside);
    };
  }, [ref, styles['active']]); // 添加样式类到依赖数组
}

2. 改用React状态控制(推荐方案)

直接操作DOM不符合React状态驱动的设计理念,建议用状态控制菜单的active类名,逻辑更可靠且符合React开发范式:

第一步:改造NavBar组件

function renderNavBar() {
  const wrapperRef = useRef<HTMLDivElement>(null);
  // 用状态管理菜单激活状态
  const [isMenuActive, setIsMenuActive] = useState(false);

  // 重写菜单点击逻辑,切换激活状态
  const handleClick = () => {
    setIsMenuActive(prev => !prev);
  };

  // 传递状态更新函数到自定义Hook
  useOutsideListener(wrapperRef, () => setIsMenuActive(false));

  return (
    <div className={styles.navBar} ref={wrapperRef}>
      <button 
        className={`${styles.menu} ${isMenuActive ? styles.active : ''}`} 
        onClick={handleClick}
      >
        MENU
      </button>
      <nav 
        className={`${styles.navContainer} ${isMenuActive ? styles.active : ''}`}
      >
        <ul className={styles.navList}>
          <Link to="/exp">Experiência</Link>
          <Link to="/formacao">Formação</Link>
          <Link to="/projects">Projetos</Link>
          <Link to="/contact">Contato</Link>
        </ul>
      </nav>
    </div>
  );
}

第二步:更新自定义Hook

function useOutsideListener(
  ref: React.MutableRefObject<null | HTMLDivElement>,
  onOutsideClick: () => void
) {
  useEffect(() => {
    function handleClickOutside(event: MouseEvent | TouchEvent) {
      if (ref.current && !ref.current.contains(event.target as Element)) {
        onOutsideClick();
      }
    }

    document.addEventListener('click', handleClickOutside);
    document.addEventListener('touchstart', handleClickOutside);

    return () => {
      document.removeEventListener('click', handleClickOutside);
      document.removeEventListener('touchstart', handleClickOutside);
    };
  }, [ref, onOutsideClick]);
}

3. 热更新后生效的原因

热更新时,Vite会重新执行组件代码,生成新的handleClickOutside回调,此时会捕获最新的DOM元素引用,因此能正常工作;而正常渲染时,回调始终使用初始渲染时的旧引用,导致无法正确操作目标元素。

内容的提问来源于stack exchange,提问作者Emanoel Mattos Ribeiro

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 18:55:17