Next.js监听路由变更时如何修改状态实现菜单开合动画
问题定位与修复方案
问题定位
- 原代码里的
useEffect未配置依赖数组,属于每次组件重渲染都会执行的副作用。长此以往会反复绑定routeChangeStart事件,导致事件监听器叠加,路由触发时会多次执行setMenuState,最终状态值因频繁覆盖出现更新异常。 - 逻辑耦合混乱:把路由事件绑定、状态更新、菜单动画三类完全独立的逻辑塞在同一个副作用里,执行时序不可控。
- 冗余代码:已经用
useRouter拿到了路由实例,根本没必要再用withRouter高阶组件包裹一层,平白增加不必要的组件重渲染。
修复后的可运行代码
import Link from 'next/link' import React, { useEffect, useState } from 'react' import { useRouter } from 'next/router' import { UpArrowCircle } from '@/components/SvgFiles' import { openMenu, closeMenu } from '@/components/MenuAnimations' const Header = ({ dimensions }) => { const [menuOpened, setMenuOpened] = useState(false) const router = useRouter() // 路由切换时自动关闭菜单,组件挂载时仅绑定一次事件 useEffect(() => { const handleRouteChange = () => { setMenuOpened(false) } // 服务端渲染阶段无window对象,加判断避免运行报错 if (typeof window !== 'undefined') { router.events.on('routeChangeStart', handleRouteChange) } // 组件卸载时清理事件监听,避免内存泄漏 return () => { if (typeof window !== 'undefined') { router.events.off('routeChangeStart', handleRouteChange) } } }, [router.events]) // 监听菜单状态变化,触发对应的开合动画 useEffect(() => { if (menuOpened) { openMenu(dimensions.width) } else { closeMenu() } }, [menuOpened, dimensions.width]) return ( <header className="header"> <div className="container"> <div className="row v-center space-between"> <div className="logo"> <Link href="/"> <a>AGENCY</a> </Link> </div> <div className="nav-toggle"> <div onClick={() => setMenuOpened(true)} className="hamburger-menu" > <span></span> <span></span> </div> <div className="hamburger-menu-close" onClick={() => setMenuOpened(false)} > <UpArrowCircle /> </div> </div> </div> </div> </header> ) } export default Header
关键优化说明
- 状态结构简化:把原来嵌套在对象里的
menuOpened拆为独立的布尔值状态,不用每次更新都新建对象引用,减少不必要的重渲染开销。 - 副作用拆分:把原来混在一起的逻辑拆成两个独立的副作用,一个负责路由事件绑定,一个负责动画触发,每个都配置正确的依赖数组,保证逻辑在正确的时机执行,不会无效重复运行。
- 兼容处理:增加客户端环境判断,不会在Next.js服务端渲染阶段因为找不到浏览器专属对象出现运行崩溃。
内容的提问来源于stack exchange,提问作者Clovis Rosa
相关产品推荐
相关产品推荐

