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

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

当前问题

  1. 点击按钮可隐藏图标并展开导航栏,但点击导航栏外部无法关闭
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 01:44:57