React中如何获取div滚动位置并设置state同步菜单状态
实现方案
监听window滚动事件失效的核心原因是:独立开启内部滚动的div的滚动事件不会冒泡到window对象,必须直接对这个滚动容器本身绑定监听,配合各内容区块的位置计算,才能实现滚动时同步更新菜单激活状态。
具体修改点
- 用
useRef获取滚动容器.contentScrollDiv的DOM实例,避免直接操作document查询 - 在
useEffect生命周期中给滚动容器绑定scroll事件,组件卸载时必须移除监听防止内存泄漏 - 补全所有内容区块的对应id,当前代码里
workhistory/certifications/education三个区块都没设置id,不仅位置计算失效,原有锚点跳转也会出问题 - 移除菜单项里嵌套的非法
<a>标签,HTML规范不允许a标签嵌套,直接用外层a标签做跳转即可 - 滚动触发时计算每个区块相对于滚动容器的偏移量,判断当前滚动位置匹配的区块索引,更新
menuIndex状态
修正后代码
App.js
import "./App.scss"; import { useState, useEffect, useRef } from 'react'; function App() { const [menuIndex, setMenuIndex] = useState(0); // 滚动容器ref const scrollContainerRef = useRef(null); // 区块id和菜单索引的映射 const sectionList = [ { id: 'summery', index: 0 }, { id: 'workhistory', index: 1 }, { id: 'certifications', index: 2 }, { id: 'education', index: 3 } ]; const menuIndexHandler = (index) => { setMenuIndex(index) } // 滚动事件处理逻辑 const handleScroll = () => { const container = scrollContainerRef.current; if (!container) return; const scrollTop = container.scrollTop; // 从后往前遍历区块,匹配当前激活项 for (let i = sectionList.length - 1; i >= 0; i--) { const section = document.getElementById(sectionList[i].id); if (!section) continue; // 减去30px偏移做提前触发,滚动体验更自然 const offsetTop = section.offsetTop - container.offsetTop - 30; if (scrollTop >= offsetTop) { setMenuIndex(sectionList[i].index); break; } } }; useEffect(() => { const container = scrollContainerRef.current; if (!container) return; // 绑定滚动监听 container.addEventListener('scroll', handleScroll); // 组件卸载时移除监听 return () => { container.removeEventListener('scroll', handleScroll); }; }, []); return ( <div className="App"> <div className="innerBox"> <div className="menu"> <div className="spaceHorizontal"></div> <a href="#summery"> <div className={`menuItem ${(menuIndex == 0) ? ' menuOpen' : ' menuClosed'} `} onClick={() =>menuIndexHandler(0)} > <label htmlFor="Summery Menu Label"> <div className="menuOpenAnimation"><span className="menuLabel">Summery</span></div> </label> </div> </a> <a href="#workhistory"> <div className={`menuItem ${(menuIndex == 1) ? ' menuOpen' : ' menuClosed'} `} onClick={() =>menuIndexHandler(1)}> <label htmlFor="Work History Menu Label"> <div className="menuOpenAnimation"><span className="menuLabel">Work History </span></div> </label> </div> </a> <a href="#certifications"> <div className={`menuItem ${(menuIndex == 2) ? ' menuOpen' : ' menuClosed'} `} onClick={() =>menuIndexHandler(2)}> <label htmlFor="Certification Menu Label"> <div className="menuOpenAnimation"><span className="menuLabel">Certifications</span></div> </label> </div> </a> <a href="#education"> <div className={`menuItem ${(menuIndex == 3) ? ' menuOpen' : ' menuClosed'} `} onClick={() =>menuIndexHandler(3)}> <label htmlFor="Education Menu Label"> <div className="menuOpenAnimation"><span className="menuLabel">Education</span></div> </label> </div> </a> </div> <div className="content"> <div className="spaceHorizontal"> <h1>Digital Resume</h1> </div> {/* 给滚动容器绑定ref */} <div className="contentScrollDiv" ref={scrollContainerRef}> <div id="summery" className="contentSubSection"> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> </div> {/* 补全缺失的区块id */} <div id="workhistory" className="contentSubSection"> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> </div> <div id="certifications" className="contentSubSection"> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> </div> <div id="education" className="contentSubSection"> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> <p> Placeholder Text </p> </div> </div> </div> </div> </div> ); } export default App;
App.scss
原有样式基本可以直接复用,只需要微调菜单文字标签的样式适配即可:
@import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); .App { background-color: #202020; height: 100vh; width: 100vw; display: flex; font-family: 'Raleway', sans-serif; } .backgroundParticles{ z-index: 1; } .innerBox { width: 900px; height: 700px; margin: auto; display: flex; flex-direction: row; z-index: 2; } .spaceHorizontal{ height: 50px; padding-bottom: 25px; } .menu { background-color: #3F485F; height: 600px; width: 200px; display: flex; flex-direction: column; border-radius: 25px 0px 0px 25px; } .menuItem { height: 40px; width: 164px; border-bottom: 1px solid white; } .menuLabel{ z-index: 3; color: white; margin-top: 15px; padding-left: 5px; display: block; } .menuOpenAnimation{ z-index: 2; width: 164px; height: 40px; position: absolute; } @keyframes menuOpenAnimation{ from {background-color: rgb(131, 197, 190); width: 0px;} to {background-color: #BC7528; width: 164px;} } .menuOpen { animation-name: menuOpenAnimation; animation-duration: 0.5s; animation-fill-mode: forwards; animation-timing-function: ease-out; background-color: #49B095; } .content { background-color: #F8F7F1; height: 600px; width: 100%; display: flex; flex-direction: column; border-radius: 0px 25px 25px 0px; } h1 { text-align: center; } hr { width: 100%; } .contentScrollDiv { height: 100%; overflow-y: scroll; scroll-behavior: smooth; padding-inline: 25px; -ms-overflow-style: none; scrollbar-width: none; } .contentScrollDiv::-webkit-scrollbar { display: none; } a { color: black; text-decoration: none; }
优化提示
如果页面内容很长、滚动频繁,可以给handleScroll方法加节流处理,降低状态更新频率,避免不必要的性能损耗。
内容的提问来源于stack exchange,提问作者Azhar Razak
相关产品推荐
相关产品推荐

