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

React菜单与子菜单的事件监听器与点击切换冲突问题

问题描述

我开发了一个带菜单和子菜单的React组件,需要实现两个核心功能:

  • 点击切换图标可打开/关闭主菜单,点击下拉图标所在行可打开/关闭子菜单;
  • 点击菜单边界外部时,自动关闭主菜单和子菜单。

目前点击打开功能正常,点击外部关闭功能也正常,但点击图标无法关闭菜单/子菜单。我试过用event.stopPropagation()和event.nativeEvent.stopImmediatePropagation()解决,但没效果。

组件完整代码
const Menubar = () => {
  const [menuOpenState, setMenuOpenState] = useState(false);
  const handleMenuClick = (event) => {
    console.log("new menuOpenState " + !menuOpenState + event.target)
    event.stopPropagation();
    event.nativeEvent.stopImmediatePropagation();
    setMenuOpenState(!menuOpenState);
  };

  const [subMenuOpenState, subMenuMenuOpenState] = useState(false);
  const handleSubMenuClick = (event) => {
    console.log("new subMenuOpenState " + !subMenuOpenState + event.target)
    event.stopPropagation();
    event.nativeEvent.stopImmediatePropagation();
    subMenuMenuOpenState(!subMenuOpenState);
  };

  const menuRef = useRef<HTMLElement>(null!)
  const subMenuRef = useRef<HTMLUListElement>(null!)
  useOnClickOutside(menuRef, () => {
    console.log("menu handler")
    setMenuOpenState(false)
  });
  useOnClickOutside(subMenuRef, () => {
    console.log("submenu handler")
    subMenuMenuOpenState(false)
  });

  return (
    <div css={menubar}>
      <div css={logoHeader}>
        <a css={logoTitle} href="#">Title</a>
      </div>
      <nav ref={menuRef} css={navHeader} className={menuOpenState ? 'menuOpen' : 'menuClosed'}>
        <ul css={menuNav}>
          <li>
            <div onClick={handleSubMenuClick} css={subMenuItem}>
              <a>Services</a>
              {subMenuOpenState ?
                  <IoChevronUp css={subMenuToggle}></IoChevronUp> :
                  <IoChevronDown css={subMenuToggle}></IoChevronDown>
              }
            </div>
            <ul ref={subMenuRef} css={subMenuNav} className={(subMenuOpenState) ? 'subMenuOpen' : 'subMenuClosed'}>
              <li><a href="#">Design</a></li>
              <li><a href="#">Development</a></li>
              <li><a href="#">SEO</a></li>
              <li><a href="#">Copywriting</a></li>
            </ul>
          </li>
          <li><a href="#">Work</a></li>
          <li><a href="#">Resources</a></li>
          <li><a href="#">About</a></li>
          <li><a href="#">Contact</a></li>
        </ul>
      </nav>

      <div css={menuToggle}>
        <a href="#" onClick={handleMenuClick}><FaBars css={hamburgerToggle} size={20}></FaBars></a>
      </div>

    </div>
  )
}
点击外部处理的自定义Hook代码
const useOnClickOutside = (ref, handler) => {
  useEffect(
    () => {
      const listener = (event) => {
        // Do nothing if clicking ref's element or descendent elements
        if (!ref.current || ref.current.contains(event.target)) {
          console.log("if condition called")
          return;
        }
        console.log("out of if 1"+ event.target)
        console.log("out of if 2"+ ref.current )

        handler(event);
      };
      document.addEventListener("mousedown", listener);
      document.addEventListener("touchstart", listener);
      return () => {
        document.removeEventListener("mousedown", listener);
        document.removeEventListener("touchstart", listener);
      };
    },
    [ref, handler]
  );
}
解决方案

问题核心是事件触发顺序冲突和ref范围设置不当:

  1. 主菜单切换按钮不在menuRef范围内,点击时useOnClickOutside的mousedown事件先触发,提前把菜单设为关闭,随后onClick又把它打开,状态抵消;
  2. 子菜单切换区域不在subMenuRef范围内(ref仅指向子菜单<ul>),点击切换区域时同样会触发外部关闭逻辑,和切换事件互相抵消。

修复步骤

1. 修复主菜单切换问题

  • 阻止切换按钮的默认跳转行为,避免页面刷新干扰状态;
  • 给切换按钮单独加ref,在外部点击监听中排除它:
// 新增汉堡按钮ref
const hamburgerRef = useRef<HTMLAnchorElement>(null!);

// 修改主菜单点击事件
const handleMenuClick = (event) => {
  event.preventDefault(); // 阻止a标签默认跳转
  setMenuOpenState(prev => !prev); // 函数式更新确保取到最新状态
};

// 修改useOnClickOutside的监听逻辑(在Hook内部或调用时传递hamburgerRef)
// 示例:在组件内调用Hook时修改listener
useOnClickOutside(menuRef, () => {
  setMenuOpenState(false)
}, hamburgerRef); // 传递汉堡按钮ref

// 对应修改Hook代码,添加排除判断
const useOnClickOutside = (ref, handler, excludeRef) => {
  useEffect(
    () => {
      const listener = (event) => {
        if (!ref.current || ref.current.contains(event.target) || (excludeRef?.current === event.target)) {
          return;
        }
        handler(event);
      };
      document.addEventListener("mousedown", listener);
      document.addEventListener("touchstart", listener);
      return () => {
        document.removeEventListener("mousedown", listener);
        document.removeEventListener("touchstart", listener);
      };
    },
    [ref, handler, excludeRef]
  );
}

// 最后给汉堡按钮绑定ref
<a ref={hamburgerRef} href="#" onClick={handleMenuClick}><FaBars css={hamburgerToggle} size={20}></FaBars></a>

2. 修复子菜单切换问题

把subMenuRef指向包含切换按钮和子菜单的父元素(<li>),这样点击切换区域时会被判定为菜单内部点击,不会触发外部关闭逻辑:

// 替换原subMenuRef
const subMenuParentRef = useRef<HTMLLIElement>(null!);

// 绑定到父li元素
<li ref={subMenuParentRef}>
  <div onClick={handleSubMenuClick} css={subMenuItem}>
    <a>Services</a>
    {subMenuOpenState ?
        <IoChevronUp css={subMenuToggle}></IoChevronUp> :
        <IoChevronDown css={subMenuToggle}></IoChevronDown>
    }
  </div>
  <ul css={subMenuNav} className={(subMenuOpenState) ? 'subMenuOpen' : 'subMenuClosed'}>
    <li><a href="#">Design</a></li>
    <li><a href="#">Development</a></li>
    <li><a href="#">SEO</a></li>
    <li><a href="#">Copywriting</a></li>
  </ul>
</li>

// 使用新的ref调用Hook
useOnClickOutside(subMenuParentRef, () => {
  subMenuMenuOpenState(false)
});

3. 额外优化

去掉没必要的event.stopPropagation()和event.nativeEvent.stopImmediatePropagation(),通过ref范围判断已经能避免事件冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 17:42:07