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
相关产品推荐
相关产品推荐

