动态导航菜单超时函数未正常清除问题排查
导航菜单下拉菜单超时未取消问题修复
我开发了一款带下拉菜单的导航组件,当用户hover菜单项时,会更新中央的NavListDropdown内容并显示。预期逻辑是:用户离开菜单项时设置1.5秒超时隐藏下拉;若用户再次hover任意菜单项,取消该超时。但快速切换hover时,之前的超时未被取消,导致下拉菜单出现异常。
原组件代码
const Nav = (props) => { const [isItemHovered, setIsItemHovered] = useState(false); const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [dropdownContent, setDropdownContent] = useState(null); const [activeItem, setActiveItem] = useState(null); const mouseEnterHandler = (content, activeItem) => { // setIsDropdownVisible(true); setIsItemHovered(true); setDropdownContent(content); setActiveItem(activeItem); }; const mouseLeaveHandler = () => { // setIsDropdownVisible(false); setIsItemHovered(false); }; useEffect(() => { let timeout; if (isItemHovered) { setIsDropdownVisible(true); } else { timeout = setTimeout(() => { setIsDropdownVisible(false); }, 1500); } return () => { if (isItemHovered && timeout) clearTimeout(timeout); }; }, [isItemHovered]); const listItems = listItemsData.map((item, i) => { return ( <NavListItem key={i} index={i} item={item} onMouseEnter={mouseEnterHandler} onMouseLeave={mouseLeaveHandler} /> ); }); return ( <nav className={classes.Nav}> <ul className={classes.NavList}>{listItems}</ul> <NavListDropdown isVisible={isDropdownVisible} content={dropdownContent} activeItem={activeItem} /> <div className={classes.NavLogo}>Logo</div> <div className={classes.NavCart}> <NavCart /> </div> </nav> ); }; export default Nav;
问题分析
原代码中,useEffect的清理函数条件错误:当isItemHovered从false切换到true时,对应false状态的effect会执行清理函数,但闭包捕获的isItemHovered值仍是false,导致if (isItemHovered && timeout)条件不成立,无法清除之前设置的隐藏超时。当超时触发时,会错误地将下拉菜单设为隐藏,即使此时用户已经hover了其他菜单项。
修复方案
使用useRef保存超时ID,确保所有状态下都能访问到最新的超时引用;同时调整清理逻辑,无论当前isItemHovered状态如何,只要存在未执行的超时就清除。
修改后的代码
import { useState, useEffect, useRef } from 'react'; const Nav = (props) => { const [isItemHovered, setIsItemHovered] = useState(false); const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [dropdownContent, setDropdownContent] = useState(null); const [activeItem, setActiveItem] = useState(null); // 用ref保存超时ID,确保跨effect访问 const timeoutRef = useRef(null); const mouseEnterHandler = (content, activeItem) => { setIsItemHovered(true); setDropdownContent(content); setActiveItem(activeItem); }; const mouseLeaveHandler = () => { setIsItemHovered(false); }; useEffect(() => { // 先清除之前可能存在的超时 if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (isItemHovered) { setIsDropdownVisible(true); } else { timeoutRef.current = setTimeout(() => { setIsDropdownVisible(false); // 超时执行后清空ref timeoutRef.current = null; }, 1500); } // 组件卸载或依赖变化时清除超时 return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [isItemHovered]); const listItems = listItemsData.map((item, i) => { return ( <NavListItem key={i} index={i} item={item} onMouseEnter={mouseEnterHandler} onMouseLeave={mouseLeaveHandler} /> ); }); return ( <nav className={classes.Nav}> <ul className={classes.NavList}>{listItems}</ul> <NavListDropdown isVisible={isDropdownVisible} content={dropdownContent} activeItem={activeItem} /> <div className={classes.NavLogo}>Logo</div> <div className={classes.NavCart}> <NavCart /> </div> </nav> ); }; export default Nav;
关键改动说明
- 添加
timeoutRef保存超时ID,避免闭包导致的旧值问题 - 每次
useEffect执行时先清除之前的超时,确保不会有遗留的超时任务 - 清理函数无条件清除超时,保证组件卸载或状态切换时不会有残留的异步操作
内容的提问来源于stack exchange,提问作者Tidris
相关产品推荐
相关产品推荐

