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

React-window:如何避免列表随状态变更重渲染并保持滚动位置?

列表状态变更后滚动位置重置?原因和解决办法看这里!

嘿,这个问题我之前做React项目时也踩过坑,咱们一步步来拆解清楚~

一、为啥会出现滚动回到顶部的情况?

本质上是列表或者它的容器在状态更新时被重新创建/完全重渲染了,浏览器会把新渲染的DOM容器的滚动位置默认重置为0。常见的触发场景有这几个:

  • 列表组件没做渲染优化,父组件状态一变,整个列表的所有DOM都被销毁后重新生成
  • 列表项的key用了不稳定的值(比如循环索引、每次渲染都变的随机数),React会把这些列表项当成全新的元素,直接全部重渲染
  • 滚动容器本身被包裹在条件渲染里,状态一变容器就被卸载再重新挂载,自然滚动位置就没了

二、怎么保持滚动位置+避免不必要重渲染?

1. 先给列表做渲染优化,减少无意义重渲染

首先用React.memo包裹列表项组件,让它只在props变化时才更新,别跟着父组件瞎凑热闹:

// 列表项组件,用React.memo包裹做浅比较
const ListItem = React.memo(({ item }) => {
  return <div className="list-item">{item.content}</div>;
});

然后给每个列表项设置稳定唯一的key,一定要用数据本身的唯一ID,别用索引!比如你的数据里有id字段就用它:

{listData.map(item => (
  <ListItem key={item.id} item={item} />
))}

2. 手动保存+恢复滚动位置

如果实在没法避免容器重渲染,那就手动把滚动位置存起来,渲染完再恢复回去:

方法一:用useRef存容器和滚动位置,结合useEffect恢复

function ScrollableList() {
  // 存滚动容器的DOM引用
  const listContainerRef = useRef(null);
  // 存滚动位置,用useRef保证状态不会触发重渲染
  const savedScrollPosition = useRef(0);

  // 列表数据变更前,先把当前滚动位置存起来
  useEffect(() => {
    if (listContainerRef.current) {
      savedScrollPosition.current = listContainerRef.current.scrollTop;
    }
  }, [listData]); // 依赖列表数据,数据变之前执行

  // 列表渲染完成后,把滚动位置恢复回去
  useEffect(() => {
    if (listContainerRef.current) {
      listContainerRef.current.scrollTop = savedScrollPosition.current;
    }
  }, [listData]);

  return (
    <div 
      ref={listContainerRef}
      style={{ height: '400px', overflowY: 'auto', border: '1px solid #eee' }}
    >
      {listData.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </div>
  );
}

方法二:用useLayoutEffect更及时恢复,避免视觉跳动

如果用上面的方法还是能看到滚动跳一下的情况,就把恢复滚动的useEffect换成useLayoutEffect,它会在DOM更新完成后同步执行,比useEffect早一步,视觉上更流畅:

useLayoutEffect(() => {
  if (listContainerRef.current) {
    listContainerRef.current.scrollTop = savedScrollPosition.current;
  }
}, [listData]);

3. 别让滚动容器被反复挂载卸载

检查下你的代码里有没有这种情况:用条件渲染直接控制滚动容器的显示隐藏,比如:

// 这种写法会让容器反复卸载挂载,滚动位置肯定没了
{isListVisible && <div ref={listContainerRef}>...</div>}

改成用style.display来控制显示隐藏,这样容器的DOM会一直保留,滚动位置自然也不会丢:

<div 
  ref={listContainerRef}
  style={{ 
    display: isListVisible ? 'block' : 'none',
    height: '400px', 
    overflowY: 'auto' 
  }}
>
  ...
</div>

三、额外小提示

如果你的列表数据特别多,用了虚拟滚动库(比如react-window、react-virtualized),这些库本身就自带了滚动位置保持的逻辑,只要你正确配置了列表项的key和数据源,根本不用自己手动处理~


内容的提问来源于stack exchange,提问作者My real name

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 20:47:32