核心用法
两种数据源: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} />}
/>