跳到主要内容

核心用法

两种数据源:data 与 totalCount

有数组就用 data;只有总数、内容按 index 算,用 totalCount

// 方式一:数据数组
<Virtuoso data={items} itemContent={(i, item) => <Row item={item} />} />

// 方式二:只知道总数(比如 10 万条)
<Virtuoso
totalCount={100000}
itemContent={(index) => <div>{index}</div>}
/>

动态高度:全自动,但可以给个估算值

react-virtuoso 最大的卖点就是动态高度开箱即用——每项高度不一致、图片加载后变高,它都用 ResizeObserver 自动测量,无需你维护高度缓存。

<Virtuoso
data={posts}
defaultItemHeight={80} // 初始估算值,测量后会逐步替换,不用很准
itemContent={(index, post) => (
<PostCard post={post} /> // 高度不可控(图文混排)也没关系
)}
/>
  • 未测量项用 defaultItemHeight(默认有一个估算值)顶着,滚动到哪测到哪,位置会越来越准;
  • 图片异步加载导致高度变化,virtuoso 会自动滚动补偿,视觉不跳动。

overscan:视口外的缓冲区

<Virtuoso
data={items}
overscan={200} // 默认 200(px)
itemContent={(i, item) => <Row item={item} />}
/>

缓冲越多,快速滚动越不容易出现白屏,但渲染的节点也越多——在"流畅"和"省资源"之间取平衡,默认值一般够用。

computeItemKey:用业务 id 做 key

列表项会被反复卸载/重建,必须用稳定且唯一的 key

<Virtuoso
data={items}
computeItemKey={(index, item) => item.id} // 用 item.id,别用 index
itemContent={(i, item) => <Row item={item} />}
/>

index 当 key 会导致滚动后状态漂移(第 100 行展开的项,滚回来变第 98 行)。

无限加载:endReached

滚动到底时追加数据:

import { useState, useCallback } from 'react';
import { Virtuoso } from 'react-virtuoso';

function Feed() {
const [items, setItems] = useState(initialItems);
const [loading, setLoading] = useState(false);

const loadMore = useCallback(async () => {
if (loading) return;
setLoading(true);
const next = await fetchPage(items.length);
setItems((prev) => [...prev, ...next]);
setLoading(false);
}, [loading, items.length]);

return (
<Virtuoso
style={{ height: '500px' }}
data={items}
endReached={loadMore} // 触底触发
itemContent={(index, item) => <Row item={item} />}
/>
);
}

头部 / 底部 / 首屏定位

<Virtuoso
data={items}
components={{
Header: () => <div className="list-header">列表标题</div>,
Footer: () => <div>没有更多了</div>,
}}
initialTopMostItemIndex={200} // 初始滚动到第 200 条附近
itemContent={(i, item) => <Row item={item} />}
/>