react-virtuoso
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesereact-virtuoso
react-virtuoso
react-virtuosotsx
import { Virtuoso } from 'react-virtuoso'
;<Virtuoso style={{ height: '100%' }} data={users} itemContent={(index, user) => <div>{user.name}</div>} />react-virtuosotsx
import { Virtuoso } from 'react-virtuoso'
;<Virtuoso style={{ height: '100%' }} data={users} itemContent={(index, user) => <div>{user.name}</div>} />Picking the right component
选择合适的组件
| Need | Use |
|---|---|
| Flat list, variable or fixed item heights | |
| Groups with sticky group headers | |
| HTML table with virtualized rows | |
| Table with grouped rows and sticky group headers | |
| Same-sized items in a responsive multi-column grid | |
| Chat / AI conversation UI (streaming, stick-to-bottom, prepend history) | |
| Data grid with columns, sorting, filtering, column features | |
All components share the same core props (/, , , scroll callbacks, ref methods).
datatotalCountitemContentcomponents| 需求 | 使用 |
|---|---|
| 高度可变或固定的扁平列表 | |
| 带粘性分组标题的分组列表 | |
| 使用虚拟化行的 HTML 表格 | |
| 带粘性分组标题的分组行表格 | |
| 响应式多列网格中的等尺寸项目 | |
| 聊天 / AI 对话 UI(流式传输、粘附底部、前置历史记录) | |
| 包含列、排序、筛选和列功能的数据表格 | |
所有组件都共享相同的核心 props(/、、、滚动回调和 ref 方法)。
datatotalCountitemContentcomponentsWhat you do NOT need to do
无需执行的操作
Unlike TanStack Virtual or react-window, react-virtuoso measures items itself. Do not carry those libraries' patterns over:
- No , no
estimateSize, nomeasureElementwiring — measurement is automatic.data-index - No absolute positioning or on items — the library positions items.
transform: translateY - No fixed requirement — variable heights are the default. If items genuinely have one uniform height, pass
itemSizeas a performance optimization only.fixedItemHeight - No windowing math — pass (or
data) and render the item intotalCount.itemContent
与 TanStack Virtual 或 react-window 不同,react-virtuoso 会自行测量项目。不要沿用这些库的模式:
- 不需要 、
estimateSize或measureElement接线——测量会自动完成。data-index - 不需要在项目上使用绝对定位或 ——库会负责项目定位。
transform: translateY - 不要求设置固定的 ——默认支持可变高度。如果项目确实具有统一高度,仅将
itemSize作为性能优化选项传入。fixedItemHeight - 不需要窗口化计算——传入 (或
data),并在totalCount中渲染项目即可。itemContent
Core rules
核心规则
- The component needs a height. Set (with a sized parent) or a fixed height. A zero-height container renders nothing.
style={{ height: '100%' }} - Never put CSS margins on items. ResizeObserver reports , which excludes margins, so the computed total height comes up short — the classic symptom is a list that cannot scroll all the way to the bottom. Use padding instead. Watch for default margins on
contentRect, headings,<p>,<ul>,<blockquote>.<pre> - Use , not
data, when you have the items. WithtotalCount,datareceives the item. UseitemContent={(index, item) => ...}only when items are derived from the index. Updates must produce a new array reference.totalCount - Provide whenever the list can be prepended, reordered, or filtered. The default key is the index, which remounts items (losing state) when positions shift.
computeItemKey={(index, item) => item.id} - Define overrides outside the render function. Inline definitions create a new component type each render, remounting the subtree on every scroll.
componentsandScrolleroverrides must forwardListto their DOM element.ref - Item content must not render zero-height elements. The error "zero-sized element, this should not happen" means an item measured 0px — filter empty items out of the data instead.
- 组件需要高度。 设置 (父元素必须有明确尺寸)或指定固定高度。高度为零的容器不会渲染任何内容。
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转发到其 DOM 元素。ref - 项目内容不得渲染零高度元素。 “zero-sized element, this should not happen” 错误表示某个项目被测量为 0px——应从数据中过滤空项目。
Common patterns
常见模式
Infinite scrolling
无限滚动
tsx
<Virtuoso data={items} endReached={() => loadMore()} itemContent={(index, item) => <Item item={item} />} />endReachedcomponents.FooterFootertsx
<Virtuoso data={items} endReached={() => loadMore()} itemContent={(index, item) => <Item item={item} />} />endReachedcomponents.FooterFooterPrepending items (reverse infinite scroll)
前置项目(反向无限滚动)
Prepending naively makes the list jump. Instead, keep a that you decrease by the number of prepended items:
firstItemIndextsx
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()}
...
/>firstItemIndex100000GroupedVirtuoso直接前置项目会导致列表跳动。相反,应维护一个 ,并按前置项目的数量递减它:
firstItemIndextsx
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()}
...
/>firstItemIndex100000GroupedVirtuosoFollowing new items (logs, feeds)
跟随新项目(日志、feed)
tsx
<Virtuoso followOutput="smooth" data={messages} ... />followOutput'auto' | 'smooth' | false(isAtBottom) => ...@virtuoso.dev/message-listtsx
<Virtuoso followOutput="smooth" data={messages} ... />followOutput'auto' | 'smooth' | false(isAtBottom) => ...@virtuoso.dev/message-listScrolling programmatically
以编程方式滚动
tsx
const ref = useRef<VirtuosoHandle>(null)
ref.current?.scrollToIndex({ index: 500, align: 'center', behavior: 'smooth' })Also on the handle: (only scrolls if not visible — right for keyboard navigation), / (pixel-based), and (snapshot for when remounting, e.g. back navigation).
scrollIntoViewscrollToscrollBygetStaterestoreStateFromTo start at an item, use — not , which first renders at the top and then jumps.
initialTopMostItemIndex={{ index, align: 'start' }}initialScrollToptsx
const ref = useRef<VirtuosoHandle>(null)
ref.current?.scrollToIndex({ index: 500, align: 'center', behavior: 'smooth' })handle 上还提供以下方法:(仅在目标不可见时滚动——适合键盘导航)、/(基于像素),以及 (生成快照,供重新挂载时通过 恢复状态,例如返回导航)。
scrollIntoViewscrollToscrollBygetStaterestoreStateFrom要从某个项目开始,请使用 ,而不是 ;后者会先在顶部渲染,然后再发生跳动。
initialTopMostItemIndex={{ index, align: 'start' }}initialScrollTopGrouped lists
分组列表
tsx
<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
/>You provide flat data plus ; map indexes back to your data yourself.
groupCountstsx
<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
/>你需要提供扁平数据和 ,并自行将索引映射回数据。
groupCountsTables
表格
tsx
<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
/>Do not set on the table — the sticky header's borders scroll away with the body. Use with explicit cell borders. Customize structure via (, , , ).
border-collapse: collapseborder-collapse: separatecomponentsTableTableRowTableHeadTableBodytsx
<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: collapseborder-collapse: separatecomponentsTableTableRowTableHeadTableBodyGrid
网格
VirtuosoGridcomponents.Item33%components.Listdisplay: flex; flex-wrap: wrapVirtuosoGridcomponents.Item33%components.Listdisplay: flex; flex-wrap: wrapPage-level scrolling
页面级滚动
Use to drive the list from the document scroll, or to attach to an existing scrollable ancestor. Pick exactly one scroll mode — combining them makes both containers scroll.
useWindowScrollcustomScrollParent={element}使用 让列表由文档滚动驱动,或使用 将其连接到已有的可滚动祖先元素。只能选择一种滚动模式——将两者结合使用会导致两个容器都发生滚动。
useWindowScrollcustomScrollParent={element}Testing
测试
JSDOM has no layout, so items will not render in Jest/Vitest without mocked measurements:
tsx
render(<Virtuoso data={data} />, {
wrapper: ({ children }) => (
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>{children}</VirtuosoMockContext.Provider>
),
})Use (adds , ) for grids. Prefer real-browser tests (Playwright) for scroll behavior.
VirtuosoGridMockContextviewportWidthitemWidthJSDOM 不具备布局功能,因此如果未模拟测量结果,项目不会在 Jest/Vitest 中渲染:
tsx
render(<Virtuoso data={data} />, {
wrapper: ({ children }) => (
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>{children}</VirtuosoMockContext.Provider>
),
})对于网格,请使用 (它会添加 和 )。涉及滚动行为的测试应优先使用真实浏览器测试(Playwright)。
VirtuosoGridMockContextviewportWidthitemWidthTroubleshooting
故障排查
| Symptom | Likely cause | Fix |
|---|---|---|
| Nothing renders | Container has zero height | Give the component or its parent a real height |
| Cannot scroll to the last items / jumps near the bottom | Margins on item content | Replace margins with padding |
| Items flicker or lose state while scrolling | Inline | Hoist components; add |
| List jumps on prepend | Items added without | Use the |
| "zero-sized element, this should not happen" | An item rendered with zero height | Filter empty items from the data |
| "ResizeObserver loop completed with undelivered notifications" overlay in dev | Benign ResizeObserver timing, surfaced by the dev-server overlay | Disable |
| Hard-to-explain size behavior | — | Set |
Tuning: renders extra pixels outside the viewport (smoother, more DOM); skips the initial probe render; skips measurement entirely (uniform items only); swaps items for placeholders during fast scrolling.
increaseViewportBydefaultItemHeightfixedItemHeightscrollSeekConfiguration| 症状 | 可能原因 | 修复方法 |
|---|---|---|
| 未渲染任何内容 | 容器高度为零 | 为组件或其父元素设置实际高度 |
| 无法滚动到最后几个项目/在底部附近跳动 | 项目内容存在外边距 | 将外边距替换为内边距 |
| 滚动时项目闪烁或丢失状态 | 内联 | 将组件定义提升到渲染函数之外;添加 |
| 前置项目时列表跳动 | 添加项目时未调整 | 使用 |
| “zero-sized element, this should not happen” | 某个项目渲染时高度为零 | 从数据中过滤空项目 |
| 开发环境中出现“ResizeObserver loop completed with undelivered notifications”浮层 | 良性的 ResizeObserver 时序问题,被开发服务器浮层显示 | 在 webpack/vite 浮层配置中禁用 |
| 尺寸行为难以解释 | — | 设置 |
调优选项: 会额外渲染视口外一定范围内的内容(滚动更平滑,但 DOM 更多); 会跳过初始探测渲染; 会完全跳过测量(仅适用于统一高度的项目); 会在快速滚动期间将项目替换为占位符。
increaseViewportBydefaultItemHeightfixedItemHeightscrollSeekConfigurationReferences
参考资料
Detailed guides with full code in references/:
- references/README.md — package overview and full feature list
- — flat list guides: 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-mode
references/1.virtuoso/ - — grouped lists: grouped-numbers, grouped-by-first-letter, scroll-to-group, grouped-with-load-on-demand
references/2.grouped-virtuoso/ - — grid-responsive-columns
references/3.virtuoso-grid/ - — tables: basic-table, table-fixed-headers, table-fixed-columns, table-grouped
references/4.table-virtuoso/ - — mocking-in-tests, tanstack-table-integration, mui-table-virtual-scroll, material-ui-endless-scrolling
references/5.third-party-integration/ - references/6.troubleshooting.md — full troubleshooting guide
Full API reference: https://virtuoso.dev/react-virtuoso/
包含完整代码的详细指南位于 references/:
- references/README.md——包概览和完整功能列表
- ——扁平列表指南: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-mode
references/1.virtuoso/ - ——分组列表:grouped-numbers、grouped-by-first-letter、scroll-to-group、grouped-with-load-on-demand
references/2.grouped-virtuoso/ - ——grid-responsive-columns
references/3.virtuoso-grid/ - ——表格:basic-table、table-fixed-headers、table-fixed-columns、table-grouped
references/4.table-virtuoso/ - ——mocking-in-tests、tanstack-table-integration、mui-table-virtual-scroll、material-ui-endless-scrolling
references/5.third-party-integration/ - references/6.troubleshooting.md——完整故障排查指南
完整 API 参考:https://virtuoso.dev/react-virtuoso/