关于 react-virtuoso
react-virtuoso 是用于 React 的虚拟列表、网格和表格组件库,只渲染可见内容,并通过 ResizeObserver 自动测量项目尺寸。适合需要处理大型数据集且项目高度可变的界面。
主要功能
- 01虚拟化列表、分组列表、表格和网格
- 02自动测量可变项目尺寸
- 03支持无限滚动与向前插入
- 04提供编程滚动和状态恢复
- 05支持窗口滚动及自定义滚动容器
适用场景
- 01渲染包含大量条目的列表或数据表
- 02实现带粘性分组标题的列表
- 03构建响应式多列虚拟网格
- 04在加载旧条目时保持列表滚动位置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ virtuoso-dev/ skills --skill react-virtuoso安装后在 Claude Code 中输入/react-virtuoso确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/1.virtuoso/auto-resizing.mdbasic-usage.mdcustom-scroll-container.mdcustomize-rendering.mdendless-scrolling.mdfooter.mdhorizontal-mode.mdinitial-index.mdkeyboard-navigation.mdpress-to-load-more.mdrange-change-callback.mdscroll-handling.mdscroll-seek-placeholders.mdscroll-to-index.mdtailwind-test.mdtop-items.mdwindow-scrolling.mdreferences/2.grouped-virtuoso/grouped-by-first-letter.mdgrouped-numbers.mdgrouped-with-load-on-demand.mdscroll-to-group.mdreferences/3.virtuoso-grid/grid-responsive-columns.mdreferences/4.table-virtuoso/basic-table.mdtable-fixed-columns.mdtable-fixed-headers.mdtable-grouped.mdreferences/5.third-party-integration/material-ui-endless-scrolling.mdmocking-in-tests.mdmui-table-virtual-scroll.mdtanstack-table-integration.md详细介绍
react-virtuoso
react-virtuoso 只渲染大型列表、网格和表格中可见的部分。它使用 ResizeObserver 自动测量项目大小——可变项目高度开箱即用,无需配置尺寸。
import { Virtuoso } from 'react-virtuoso'
;<Virtuoso style={{ height: '100%' }} data={users} itemContent={(index, user) => <div>{user.name}</div>} />
选择合适的组件
| 需求 | 使用 |
|---|---|
| 平面列表,项目高度可变或固定 | Virtuoso |
| 带有粘性分组标题的分组 | GroupedVirtuoso |
| 对行进行虚拟化的 HTML 表格 | TableVirtuoso |
| 带有分组行和粘性分组标题的表格 | GroupedTableVirtuoso |
| 响应式多列网格中的等尺寸项目 | VirtuosoGrid |
| 聊天 / AI 对话 UI(流式输出、贴底、前置历史记录) | @virtuoso.dev/message-list — 改用 message-list 技能 |
| 带有列、排序、筛选和列功能的数据网格 | @virtuoso.dev/data-table — 改用 data-table 技能 |
所有组件共享相同的核心属性(data/totalCount、itemContent、components、滚动回调、ref 方法)。
无需做的事情
与 TanStack Virtual 或 react-window 不同,react-virtuoso 会自行测量项目。不要照搬这些库的模式:
- 无需
estimateSize、measureElement或data-index配置——测量会自动进行。 - 不要在项目上使用绝对定位或
transform: translateY——库会为项目定位。 - 无需固定的
itemSize——默认支持可变高度。只有在项目确实高度统一时,才传入fixedItemHeight作为性能优化。 - 无需计算窗口化逻辑——传入
data(或totalCount),并在itemContent中渲染项目即可。
核心规则
- 组件需要设置高度。 设置
style={{ height: '100%' }}(父元素需有明确高度)或固定高度。高度为零的容器不会渲染任何内容。 - 不要在项目上设置 CSS 外边距。 ResizeObserver 报告的
contentRect不包含外边距,因此计算出的总高度会偏小——典型表现是列表无法滚动到底部。请改用内边距。注意<p>、标题、<ul>、<blockquote>、<pre>的默认外边距。 - 有项目时使用
data,不要使用totalCount。 使用data时,itemContent={(index, item) => ...}会接收项目。只有在项目由索引派生时才使用totalCount。更新时必须生成新的数组引用。 - 只要列表可能进行前置添加、重新排序或筛选,就应提供
computeItemKey={(index, item) => item.id}。 默认键为索引,位置变化时会重新挂载项目(导致状态丢失)。 - 在渲染函数外定义
components覆盖项。 内联定义会在每次渲染时创建新的组件类型,导致每次滚动都重新挂载子树。Scroller和List覆盖项必须将ref转发到其 DOM 元素。 - 项目内容不能渲染高度为零的元素。 报错 "zero-sized element, this should not happen" 表示某个项目测得高度为 0px——请从数据中筛除空项目。
常见模式
无限滚动
<Virtuoso data={items} endReached={() => loadMore()} itemContent={(index, item) => <Item item={item} />} />
endReached 会在滚动到底部时触发;通过 components.Footer 渲染加载指示器。若要点击“加载更多”,请将按钮放在 Footer 中。
前置添加项目(反向无限滚动)
直接前置添加会导致列表跳动。请改为维护 firstItemIndex,并将其减去前置添加的项目数:
const [firstItemIndex, setFirstItemIndex] = useState(START)
const prepend = async () => {
const older = await fetchOlderItems()
setFirstItemIndex((i) => i - older.length)
setItems((current) => [...older, ...current])
}
<Virtuoso
computeItemKey={(_, item) => item.id}
data={items}
firstItemIndex={firstItemIndex}
initialTopMostItemIndex={{ index: 'LAST' }}
startReached={() => void prepend()}
...
/>
firstItemIndex 必须保持为正数,因此初始值要设得较大(例如 100000)。对于 GroupedVirtuoso,只减去新项目的数量,不包括分组标题。请参阅 endless-scrolling。
跟随新项目(日志、信息流)
<Virtuoso followOutput="smooth" data={messages} ... />
只有当用户已位于底部时,followOutput 才会滚动到新增的底部项目。它接受 'auto' | 'smooth' | false,也可以接受函数 (isAtBottom) => ... 来实现自定义逻辑。对于完整的聊天界面,建议使用 @virtuoso.dev/message-list。
以编程方式滚动
const ref = useRef<VirtuosoHandle>(null)
ref.current?.scrollToIndex({ index: 500, align: 'center', behavior: 'smooth' })
句柄上还提供:scrollIntoView(仅在目标不可见时滚动,适用于键盘导航)、scrollTo/scrollBy(基于像素滚动)以及 getState(在重新挂载时用于 restoreStateFrom 的快照,例如返回导航)。
要从某个项目开始显示,请使用 initialTopMostItemIndex={{ index, align: 'start' }},不要使用 initialScrollTop,后者会先从顶部渲染,然后再跳转。
分组列表
<GroupedVirtuoso
groupCounts={[20, 30]} // items per group, in order
groupContent={(groupIndex) => <Header group={groups[groupIndex]} />}
itemContent={(index, groupIndex) => <Item item={items[index]} />} // index is absolute across all items
/>
你需要提供扁平数据和 groupCounts,并自行将索引映射回数据。
表格
<TableVirtuoso
data={rows}
fixedHeaderContent={() => (
<tr>
<th>Name</th>
</tr>
)}
itemContent={(index, row) => (
<>
<td>{row.name}</td>
</>
)} // <td> cells only — the row <tr> is rendered for you
/>
不要在表格上设置 border-collapse: collapse,否则粘性表头的边框会随表格主体一起滚动。请使用 border-collapse: separate 并显式设置单元格边框。可通过 components(Table、TableRow、TableHead、TableBody)自定义结构。
网格
VirtuosoGrid 会按列虚拟化尺寸相同的项目。你可以通过 CSS 控制列数:为 components.Item 设置百分比宽度(三列时为 33%,可通过媒体查询调整),并为 components.List 设置 display: flex; flex-wrap: wrap。请参阅 grid-responsive-columns。
页面级滚动
使用 useWindowScroll 让列表跟随文档滚动,或使用 customScrollParent={element} 将其附加到现有的可滚动祖先元素。只能选择一种滚动模式,同时使用会导致两个容器都发生滚动。
测试
JSDOM 不支持布局,因此如果不模拟测量值,Jest/Vitest 中的项目将不会渲染:
render(<Virtuoso data={data} />, {
wrapper: ({ children }) => (
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>{children}</VirtuosoMockContext.Provider>
),
})
对于网格,请使用 VirtuosoGridMockContext(添加 viewportWidth、itemWidth)。滚动行为测试优先使用真实浏览器(Playwright)。
故障排查
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| 没有任何内容渲染 | 容器高度为零 | 为组件或其父元素设置实际高度 |
| 无法滚动到最后几项/滚动位置跳到接近底部 | 项目内容使用了外边距 | 将外边距替换为内边距 |
| 滚动时项目闪烁或丢失状态 | 内联的 components 定义或基于索引的键 | 将组件提取到外部定义;添加 computeItemKey |
| 在前置插入项目时列表跳动 | 添加项目时未调整 firstItemIndex | 使用 firstItemIndex 前置插入模式 |
| “zero-sized element, this should not happen” | 渲染的项目高度为零 | 从数据中筛除空项目 |
| 开发环境中出现 “ResizeObserver loop completed with undelivered notifications” 覆盖层 | 无害的 ResizeObserver 时序问题,由开发服务器覆盖层显示出来 | 在 webpack/vite 覆盖层配置中禁用 runtimeErrors;在错误跟踪中安全过滤 |
| 难以解释的尺寸行为 | — | 设置 logLevel={LogLevel.DEBUG},并在启用所有级别的情况下查看控制台 |
调优:increaseViewportBy 会在视口外额外渲染一些像素(更流畅,但 DOM 更多);defaultItemHeight 会跳过初始探测渲染;fixedItemHeight 会完全跳过测量(仅适用于所有项目高度一致的情况);scrollSeekConfiguration 会在快速滚动期间将项目替换为占位符。
参考资料
完整代码详解指南见 references/:
- references/README.md — 软件包概览和完整功能列表
references/1.virtuoso/— 平铺列表指南:basic-usage、endless-scrolling、press-to-load-more、initial-index、scroll-to-index、keyboard-navigation、customize-rendering、footer、top-items、scroll-handling、range-change-callback、scroll-seek-placeholders、auto-resizing、window-scrolling、custom-scroll-container、horizontal-modereferences/2.grouped-virtuoso/— 分组列表:grouped-numbers、grouped-by-first-letter、scroll-to-group、grouped-with-load-on-demandreferences/3.virtuoso-grid/— grid-responsive-columnsreferences/4.table-virtuoso/— 表格:basic-table、table-fixed-headers、table-fixed-columns、table-groupedreferences/5.third-party-integration/— mocking-in-tests、tanstack-table-integration、mui-table-virtual-scroll、material-ui-endless-scrolling- references/6.troubleshooting.md — 完整故障排除指南
完整 API 参考文档:https://virtuoso.dev/react-virtuoso/