React中移动端菜单打开时禁止页面垂直滚动的实现方案
移动端导航栏菜单联动滚动锁定修复方案
问题根源
原有代码存在4个核心问题导致功能异常:
- 滚动锁定逻辑没有和菜单开关状态
isOpen绑定,直接全局设置overflow: hidden导致页面永久锁死 - 导航栏背景切换的滚动事件直接写在组件函数体内,每次组件重渲染都会重复绑定事件,既造成内存泄漏,也会干扰滚动逻辑
- 滚动隐藏导航栏的逻辑没有做菜单状态拦截,就算触发异常滚动也会把导航栏连带菜单移出视口
- iOS端仅设置
overflow: hidden无法完全锁定触摸滚动,会出现滚动穿透问题
修复步骤
- 新增独立的
useEffect监听菜单开关状态,菜单打开时锁定页面滚动,关闭/组件卸载时还原滚动状态,同时记录滚动位置避免锁滚动后页面跳回顶部 - 在导航栏滚动隐藏逻辑里加前置拦截,菜单打开时直接跳过隐藏判断
- 把散落在组件函数体里的导航栏背景/阴影切换逻辑合并到统一的滚动处理函数中,删除重复绑定的滚动事件
- 修正原有代码里
boxShadow样式赋值的语法错误(之前多写了一层box-shadow:属性名前缀,导致样式不生效)
修复后完整代码
import * as React from 'react'; import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { Squash as Hamburger } from 'hamburger-react'; import { debounce } from './utilities/helpers'; const links = [ {name: "Home", path: "/"}, {name: "Projects", path: "/projects"}, {name: "Contact", path: "/contact"} ]; export default function Header() { // 导航栏滚动隐藏状态 const [prevScrollPos, setPrevScrollPos] = useState(0); const [visible, setVisible] = useState(true); // 汉堡菜单状态 const [isOpen, setOpen] = useState(false); // 统一滚动处理逻辑 const handleScroll = debounce(() => { // 菜单打开时跳过导航栏隐藏逻辑 if (isOpen) return; const currentScrollPos = window.pageYOffset; // 导航栏显隐判断 setVisible((prevScrollPos > currentScrollPos && prevScrollPos - currentScrollPos > 75) || currentScrollPos < 10); setPrevScrollPos(currentScrollPos); // 导航栏背景和阴影切换 const navBar = document.getElementById('navbar'); if(currentScrollPos >= 1){ navBar.style.background = '#181a1d'; navBar.style.boxShadow = '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'; } else { navBar.style.background = 'none'; navBar.style.boxShadow = 'none' } }, 100); // 绑定导航栏滚动监听 useEffect(() => { window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [prevScrollPos, visible, isOpen, handleScroll]); // 菜单打开时锁定页面滚动 useEffect(() => { if (isOpen) { const scrollY = window.scrollY; // 兼容iOS滚动穿透问题 document.body.style.position = 'fixed'; document.body.style.top = `-${scrollY}px`; document.body.style.width = '100%'; document.body.style.overflow = 'hidden'; } else { const scrollY = document.body.style.top; // 还原页面样式 document.body.style.position = ''; document.body.style.top = ''; document.body.style.width = ''; document.body.style.overflow = ''; // 还原原有滚动位置 window.scrollTo(0, parseInt(scrollY || '0', 10) * -1); } // 组件卸载(路由跳转)时自动解除滚动锁定 return () => { document.body.style.position = ''; document.body.style.top = ''; document.body.style.width = ''; document.body.style.overflow = ''; } }, [isOpen]); return ( <nav style={{ top: visible ? '0' : '-75px' }} id="navbar" className="w-full h-[75px] fixed py-2 px-3 sm:px-6 flex justify-between items-center z-20 transition-all duration-300 ease-in-out"> {/* Logo区域 */} <Link to="/"> <div className="bg-[url('../public/Images/logo.png')] hover:bg-[url('../public/Images/logo-hover.png')] w-[260px] h-[60px]"></div> </Link> <div className="flex"> {/* 汉堡按钮 */} <div className="lg:hidden mb-0.5 z-10"> <Hamburger color="#ffffff" easing="ease-in-out" rounded toggled={isOpen} toggle={setOpen} /> </div> {/* 移动端菜单内容 */} {isOpen && ( <> <div className="h-screen w-screen absolute mt-[3.75rem] left-0 bg-black opacity-60 blur-sm"></div> <ul className="h-screen w-3/5 sm:w-1/2 md:w-2/5 bg-quaternary flex flex-col lg:flex-row items-center absolute top-0 right-0 pt-32"> {links.map((link) => ( <li key={link.path} className="m-5 lg:my-0 lg:mx-5 leading-10"> <Link className="text-2xl font-medium text-white hover:text-primary transition duration-300 ease-in-out" to={link.path} onClick={() => setOpen(false)} > {link.name} </Link> </li> ))} </ul> </> )} {/* 端导航链接 */} <ul className= "hidden lg:flex flex-row"> {links.map((link) => ( <li key={link.path} className="m-5 lg:my-0 lg:mx-5"> <Link className="text-2xl font-medium text-white hover:text-primary transition duration-300 ease-in-out" to={link.path}> {link.name} </Link> </li> ))} </ul> </div> </nav> ); }
额外优化点
- 给循环渲染的
li元素补充了key属性,避免React渲染警告 - 删除了冗余的
ToggleNavLinks函数,该函数逻辑已经合并到滚动处理中,且汉堡组件本身已经绑定了开关状态 - 给滚动监听的useEffect依赖数组补充了
isOpen,保证状态变化时逻辑同步
内容的提问来源于stack exchange,提问作者Josh Haywood
相关产品推荐
相关产品推荐

