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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 09:57:34