Next.js中React useState结合事件监听器失效问题求助
汉堡导航栏交互问题排查与修复
问题背景
我正在开发一个网页,需求为右上角固定汉堡图标,点击可展开导航栏,点击导航栏外部则关闭导航栏并重新显示汉堡图标。由于Next.js服务端组件不支持用户交互,创建了CrMenuWrapper客户端组件,代码如下:
'use client' import logger from '@/utils/logger' import { NextPage } from 'next' import { useEffect, useRef, useState } from 'react' interface Props { } const CrMenuWrapper: NextPage<Props> = ({ }) => { const [isMenuClicked, setIsMenuClicked] = useState(false) const phoneMenu: React.RefObject<HTMLDivElement> = useRef(null) const menuButton: React.RefObject<HTMLDivElement> = useRef(null) logger(isMenuClicked) function handleOnClick(e: any) { if (phoneMenu.current && isMenuClicked && !phoneMenu.current.contains(e.target)) { setIsMenuClicked(prevIsMenuClicked => !prevIsMenuClicked) } else if (menuButton.current && !isMenuClicked && menuButton.current.contains(e.target)) { logger("clicked") setIsMenuClicked(prevIsMenuClicked => !prevIsMenuClicked) } } useEffect(() => { document.addEventListener('click', (e) => handleOnClick(e)) return () => { document.removeEventListener('click', (e) => handleOnClick(e)) }; }, []) return <div> <div ref={phoneMenu} className={`fixed z-50 bg-gradient-to-bl from-violet-600/30 backdrop-blur-sm to-indigo-600/30 bg-no-repeat bg-top top-0 right-0 transition-all border-2 border-[var(--secondary-rgb)] w-[75vw] aspect-square rounded-es-full md:hidden origin-top-right duration-300 ${isMenuClicked ? " scale-100 rotate-0" : " scale-0 rotate-45"}`}> </div> <div ref={menuButton} className={` bg-transparent md:hidden fixed aspect-square h-12 top-10 backdrop-blur-md right-10 stroke-0 hover:cursor-pointer border-[var(--secondary-rgb)] p-2.5 rounded-full border-2 hover:scale-125 transition-all opacity-70 hover:opacity-100 ${isMenuClicked ? " scale-0" : " scale-100"}`}> <svg id="nav-svg" stroke="currentColor" fill="currentColor" className={`fill-[var(--primary-rgb)] `} viewBox="0 0 24 24" height="inherit" width="inherit" xmlns="http://www.w3.org/2000/svg"><path id="nav-path" d="M16 18V20H5V18H16ZM21 11V13H3V11H21ZM19 4V6H8V4H19Z"></path></svg> </div> </div> } export default CrMenuWrapper
当前问题
- 点击按钮可隐藏图标并展开导航栏,但点击导航栏外部无法关闭
handleOnClick函数内打印的isMenuClicked始终为false,组件外层打印的该状态却能正确更新
问题根源
事件监听器在组件挂载时绑定,此时handleOnClick函数捕获的是初始状态isMenuClicked = false。后续状态更新后,监听器依然引用旧的函数闭包,导致内部始终读取旧的状态值,无法响应最新状态变化。
修复方案
1. 更新事件监听器依赖
将isMenuClicked和handleOnClick加入useEffect的依赖数组,确保状态变化时重新绑定事件监听器,让函数能获取最新状态。
2. 简化事件处理逻辑
拆分判断分支,避免条件嵌套导致的逻辑遗漏,同时明确不同场景下的状态切换规则。
修复后的完整代码
'use client' import logger from '@/utils/logger' import { NextPage } from 'next' import { useEffect, useRef, useState, useCallback } from 'react' interface Props { } const CrMenuWrapper: NextPage<Props> = ({ }) => { const [isMenuClicked, setIsMenuClicked] = useState(false) const phoneMenu: React.RefObject<HTMLDivElement> = useRef(null) const menuButton: React.RefObject<HTMLDivElement> = useRef(null) logger(isMenuClicked) // 使用useCallback缓存函数,减少不必要的重渲染 const handleOnClick = useCallback((e: any) => { // 点击汉堡按钮时切换菜单状态 if (menuButton.current?.contains(e.target)) { setIsMenuClicked(prev => !prev) return } // 菜单展开状态下,点击导航栏外部则关闭菜单 if (isMenuClicked && phoneMenu.current && !phoneMenu.current.contains(e.target)) { setIsMenuClicked(false) } }, [isMenuClicked]) useEffect(() => { document.addEventListener('click', handleOnClick) return () => { document.removeEventListener('click', handleOnClick) }; }, [handleOnClick]) // 添加依赖,确保函数更新时重新绑定 return <div> <div ref={phoneMenu} className={`fixed z-50 bg-gradient-to-bl from-violet-600/30 backdrop-blur-sm to-indigo-600/30 bg-no-repeat bg-top top-0 right-0 transition-all border-2 border-[var(--secondary-rgb)] w-[75vw] aspect-square rounded-es-full md:hidden origin-top-right duration-300 ${isMenuClicked ? " scale-100 rotate-0" : " scale-0 rotate-45"}`}> </div> <div ref={menuButton} className={` bg-transparent md:hidden fixed aspect-square h-12 top-10 backdrop-blur-md right-10 stroke-0 hover:cursor-pointer border-[var(--secondary-rgb)] p-2.5 rounded-full border-2 hover:scale-125 transition-all opacity-70 hover:opacity-100 ${isMenuClicked ? " scale-0" : " scale-100"}`}> <svg id="nav-svg" stroke="currentColor" fill="currentColor" className={`fill-[var(--primary-rgb)] `} viewBox="0 0 24 24" height="inherit" width="inherit" xmlns="http://www.w3.org/2000/svg"><path id="nav-path" d="M16 18V20H5V18H16ZM21 11V13H3V11H21ZM19 4V6H8V4H19Z"></path></svg> </div> </div> } export default CrMenuWrapper
内容的提问来源于stack exchange,提问作者MrPand
相关产品推荐
相关产品推荐

