React下拉组件点击标题时如何阻止滚动位置跳至底部
解决点击展开下拉项时页面滚动位置跳到底部的问题
问题重现
我做了一个标题列表,点击标题会展开对应的子标题列表,但操作时出现异常:先展开第一个标题,滚动到第八个标题点击后,页面直接跳到底部,只能看到最后几个子标题。简化后的示例代码如下:
import { useState } from 'react'; const Hold = () => { interface TitleItem { id: number; title: string; } interface SubTitleItem { id: number; subTitle: string; } const titleArray: TitleItem[] = [ { id: 1, title: "First Item" }, { id: 2, title: "Second Item" }, { id: 3, title: "Third Item" }, { id: 4, title: "Fourth Item" }, { id: 5, title: "Fifth Item" }, { id: 6, title: "Sixth Item" }, { id: 7, title: "Seventh Item" }, { id: 8, title: "Eighth Item" }, { id: 9, title: "Ninth Item" }, { id: 10, title: "Tenth Item" }, ]; const subTitleArray: SubTitleItem[] = [ { id: 1, subTitle: "First Sub Item" }, { id: 2, subTitle: "Second Sub Item" }, { id: 3, subTitle: "Third Sub Item" }, { id: 4, subTitle: "Fourth Sub Item" }, { id: 5, subTitle: "Fifth Sub Item" }, { id: 6, subTitle: "Sixth Sub Item" }, { id: 7, subTitle: "Seventh Sub Item" }, { id: 8, subTitle: "Eighth Sub Item" }, { id: 9, subTitle: "Ninth Sub Item" }, { id: 10, subTitle: "Tenth Sub Item" }, ]; const [selectedTitle, setSelectedTitle] = useState<TitleItem | null>(null); return ( <div className="p-10 h-screen"> {titleArray.map((title) => ( <div key={title.id}> <div onClick={() => { setSelectedTitle(title); }} > {title.title} </div> {selectedTitle && selectedTitle.id === title.id && ( <div> {subTitleArray.map((subTitle) => ( <div className="h-28 border p-5 mb-2" key={subTitle.id}> {subTitle.subTitle} </div> ))} </div> )} </div> ))} </div> ); }; export default Hold;
问题原因
这是浏览器滚动锚定行为导致的:当你点击新标题时,之前展开的子项DOM会被卸载,页面整体高度突然缩小,浏览器会尝试维持当前的相对滚动位置;而新展开的子项又会瞬间增加页面高度,两者叠加就会让滚动位置被“推”到底部。
解决方案
下面提供三种实用的解决方法,按需选择:
方法1:禁用浏览器滚动锚定(最简单)
给最外层容器添加CSS属性,直接阻止浏览器自动调整滚动位置:
<div className="p-10 h-screen" style={{ overflowAnchor: "none", scrollBehavior: "auto" }}>
overflow-anchor: none:禁用滚动锚定,告诉浏览器不要自动调整滚动位置scrollBehavior: auto:确保滚动行为是即时的,避免过渡动画干扰
方法2:手动保存并恢复滚动位置(精准控制)
通过useRef记录点击时的滚动位置,在DOM更新后立即恢复:
import { useState, useRef, useLayoutEffect } from 'react'; const Hold = () => { // ... 接口和数组定义不变 const [selectedTitle, setSelectedTitle] = useState<TitleItem | null>(null); const scrollPositionRef = useRef<number>(0); const handleTitleClick = (title: TitleItem) => { // 点击时记录当前滚动位置 scrollPositionRef.current = window.scrollY; setSelectedTitle(title); }; useLayoutEffect(() => { // DOM更新完成后,恢复之前的滚动位置 window.scrollTo(0, scrollPositionRef.current); }, [selectedTitle]); return ( <div className="p-10 h-screen"> {titleArray.map((title) => ( <div key={title.id}> <div onClick={() => handleTitleClick(title)}> {title.title} </div> {selectedTitle && selectedTitle.id === title.id && ( <div> {subTitleArray.map((subTitle) => ( <div className="h-28 border p-5 mb-2" key={subTitle.id}> {subTitle.subTitle} </div> ))} </div> )} </div> ))} </div> ); }; export default Hold;
方法3:支持多展开(避免DOM卸载)
如果业务允许同时展开多个标题,可以把单选中改成多选中,这样不会卸载之前的子项DOM,页面高度变化更平缓:
import { useState } from 'react'; const Hold = () => { // ... 接口和数组定义不变 // 用数组记录所有选中的标题ID const [selectedTitleIds, setSelectedTitleIds] = useState<number[]>([]); const handleTitleClick = (titleId: number) => { setSelectedTitleIds(prev => // 切换选中状态:已选中则移除,未选中则添加 prev.includes(titleId) ? prev.filter(id => id !== titleId) : [...prev, titleId] ); }; return ( <div className="p-10 h-screen"> {titleArray.map((title) => ( <div key={title.id}> <div onClick={() => handleTitleClick(title.id)}> {title.title} </div> // 判断当前标题是否在选中数组中 {selectedTitleIds.includes(title.id) && ( <div> {subTitleArray.map((subTitle) => ( <div className="h-28 border p-5 mb-2" key={subTitle.id}> {subTitle.subTitle} </div> ))} </div> )} </div> ))} </div> ); }; export default Hold;
内容的提问来源于stack exchange,提问作者Krneki123
相关产品推荐
相关产品推荐

