react-virtuoso

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

react-virtuoso

react-virtuoso

react-virtuoso
renders only the visible portion of large lists, grids, and tables. It measures item sizes automatically with ResizeObserver — variable item heights work out of the box, with no size configuration.
tsx
import { Virtuoso } from 'react-virtuoso'
;<Virtuoso style={{ height: '100%' }} data={users} itemContent={(index, user) => <div>{user.name}</div>} />
react-virtuoso
仅渲染大型列表、网格和表格中可见的部分。它通过 ResizeObserver 自动测量项目尺寸——开箱即用地支持可变项目高度,无需配置尺寸。
tsx
import { Virtuoso } from 'react-virtuoso'
;<Virtuoso style={{ height: '100%' }} data={users} itemContent={(index, user) => <div>{user.name}</div>} />

Picking the right component

选择合适的组件

NeedUse
Flat list, variable or fixed item heights
Virtuoso
Groups with sticky group headers
GroupedVirtuoso
HTML table with virtualized rows
TableVirtuoso
Table with grouped rows and sticky group headers
GroupedTableVirtuoso
Same-sized items in a responsive multi-column grid
VirtuosoGrid
Chat / AI conversation UI (streaming, stick-to-bottom, prepend history)
@virtuoso.dev/message-list
— use the
message-list
skill instead
Data grid with columns, sorting, filtering, column features
@virtuoso.dev/data-table
— use the
data-table
skill instead
All components share the same core props (
data
/
totalCount
,
itemContent
,
components
, scroll callbacks, ref methods).
需求使用
高度可变或固定的扁平列表
Virtuoso
带粘性分组标题的分组列表
GroupedVirtuoso
使用虚拟化行的 HTML 表格
TableVirtuoso
带粘性分组标题的分组行表格
GroupedTableVirtuoso
响应式多列网格中的等尺寸项目
VirtuosoGrid
聊天 / AI 对话 UI(流式传输、粘附底部、前置历史记录)
@virtuoso.dev/message-list
——改用
message-list
skill
包含列、排序、筛选和列功能的数据表格
@virtuoso.dev/data-table
——改用
data-table
skill
所有组件都共享相同的核心 props(
data
/
totalCount
、
itemContent
、
components
、滚动回调和 ref 方法)。

What 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
    estimateSize
    , no
    measureElement
    , no
    data-index
    wiring — measurement is automatic.
  • No absolute positioning or
    transform: translateY
    on items — the library positions items.
  • No fixed
    itemSize
    requirement — variable heights are the default. If items genuinely have one uniform height, pass
    fixedItemHeight
    as a performance optimization only.
  • No windowing math — pass
    data
    (or
    totalCount
    ) and render the item in
    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
    style={{ height: '100%' }}
    (with a sized parent) or a fixed height. A zero-height container renders nothing.
  • Never put CSS margins on items. ResizeObserver reports
    contentRect
    , 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
    <p>
    , headings,
    <ul>
    ,
    <blockquote>
    ,
    <pre>
    .
  • Use
    data
    , not
    totalCount
    , when you have the items.
    With
    data
    ,
    itemContent={(index, item) => ...}
    receives the item. Use
    totalCount
    only when items are derived from the index. Updates must produce a new array reference.
  • Provide
    computeItemKey={(index, item) => item.id}
    whenever the list can be prepended, reordered, or filtered. The default key is the index, which remounts items (losing state) when positions shift.
  • Define
    components
    overrides outside the render function.
    Inline definitions create a new component type each render, remounting the subtree on every scroll.
    Scroller
    and
    List
    overrides must forward
    ref
    to their DOM element.
  • 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
    覆盖组件必须将
    ref
    转发到其 DOM 元素。
  • 项目内容不得渲染零高度元素。 “zero-sized element, this should not happen” 错误表示某个项目被测量为 0px——应从数据中过滤空项目。

Common patterns

常见模式

Infinite scrolling

无限滚动

tsx
<Virtuoso data={items} endReached={() => loadMore()} itemContent={(index, item) => <Item item={item} />} />
endReached
fires at the bottom; render a spinner via
components.Footer
. For "load more" on click, put the button in
Footer
.
tsx
<Virtuoso data={items} endReached={() => loadMore()} itemContent={(index, item) => <Item item={item} />} />
endReached
会在底部触发;通过
components.Footer
渲染加载指示器。如需点击“加载更多”,请将按钮放在
Footer
中。

Prepending items (reverse infinite scroll)

前置项目(反向无限滚动)

Prepending naively makes the list jump. Instead, keep a
firstItemIndex
that you decrease by the number of prepended items:
tsx
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
must stay a positive number, so start it large (e.g.
100000
). For
GroupedVirtuoso
, decrease it by the number of new items only, excluding the group headers. See endless-scrolling.
直接前置项目会导致列表跳动。相反,应维护一个
firstItemIndex
,并按前置项目的数量递减它:
tsx
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。

Following new items (logs, feeds)

跟随新项目(日志、feed)

tsx
<Virtuoso followOutput="smooth" data={messages} ... />
followOutput
scrolls to new bottom items only when the user is already at the bottom. It accepts
'auto' | 'smooth' | false
or a function
(isAtBottom) => ...
for custom logic. For full chat UIs prefer
@virtuoso.dev/message-list
.
tsx
<Virtuoso followOutput="smooth" data={messages} ... />
followOutput
仅在用户已经位于底部时,才会滚动至新的底部项目。它接受
'auto' | 'smooth' | false
,也可传入函数
(isAtBottom) => ...
实现自定义逻辑。完整的聊天 UI 应优先使用
@virtuoso.dev/message-list
。

Scrolling programmatically

以编程方式滚动

tsx
const ref = useRef<VirtuosoHandle>(null)
ref.current?.scrollToIndex({ index: 500, align: 'center', behavior: 'smooth' })
Also on the handle:
scrollIntoView
(only scrolls if not visible — right for keyboard navigation),
scrollTo
/
scrollBy
(pixel-based), and
getState
(snapshot for
restoreStateFrom
when remounting, e.g. back navigation).
To start at an item, use
initialTopMostItemIndex={{ index, align: 'start' }}
— not
initialScrollTop
, which first renders at the top and then jumps.
tsx
const ref = useRef<VirtuosoHandle>(null)
ref.current?.scrollToIndex({ index: 500, align: 'center', behavior: 'smooth' })
handle 上还提供以下方法:
scrollIntoView
(仅在目标不可见时滚动——适合键盘导航)、
scrollTo
/
scrollBy
(基于像素),以及
getState
(生成快照,供重新挂载时通过
restoreStateFrom
恢复状态,例如返回导航)。
要从某个项目开始,请使用
initialTopMostItemIndex={{ index, align: 'start' }}
,而不是
initialScrollTop
;后者会先在顶部渲染,然后再发生跳动。

Grouped 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
groupCounts
; map indexes back to your data yourself.
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
/>
你需要提供扁平数据和
groupCounts
,并自行将索引映射回数据。

Tables

表格

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
border-collapse: collapse
on the table — the sticky header's borders scroll away with the body. Use
border-collapse: separate
with explicit cell borders. Customize structure via
components
(
Table
,
TableRow
,
TableHead
,
TableBody
).
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
/>
不要在表格上设置
border-collapse: collapse
——粘性标题的边框会随主体内容一起滚动消失。请使用
border-collapse: separate
,并显式设置单元格边框。可通过
components
(
Table
、
TableRow
、
TableHead
、
TableBody
)自定义结构。

Grid

网格

VirtuosoGrid
virtualizes same-sized items in columns. You control column count with CSS — give
components.Item
a percentage width (
33%
for three columns, changed via media queries) and
components.List
display: flex; flex-wrap: wrap
. See grid-responsive-columns.
VirtuosoGrid
会对列中的等尺寸项目进行虚拟化。你可以通过 CSS 控制列数——为
components.Item
设置百分比宽度(三列使用
33%
,也可通过媒体查询更改),并为
components.List
设置
display: flex; flex-wrap: wrap
。请参阅 grid-responsive-columns。

Page-level scrolling

页面级滚动

Use
useWindowScroll
to drive the list from the document scroll, or
customScrollParent={element}
to attach to an existing scrollable ancestor. Pick exactly one scroll mode — combining them makes both containers scroll.
使用
useWindowScroll
让列表由文档滚动驱动,或使用
customScrollParent={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
VirtuosoGridMockContext
(adds
viewportWidth
,
itemWidth
) for grids. Prefer real-browser tests (Playwright) for scroll behavior.
JSDOM 不具备布局功能,因此如果未模拟测量结果,项目不会在 Jest/Vitest 中渲染:
tsx
render(<Virtuoso data={data} />, {
  wrapper: ({ children }) => (
    <VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>{children}</VirtuosoMockContext.Provider>
  ),
})
对于网格,请使用
VirtuosoGridMockContext
(它会添加
viewportWidth
和
itemWidth
)。涉及滚动行为的测试应优先使用真实浏览器测试(Playwright)。

Troubleshooting

故障排查

SymptomLikely causeFix
Nothing rendersContainer has zero heightGive the component or its parent a real height
Cannot scroll to the last items / jumps near the bottomMargins on item contentReplace margins with padding
Items flicker or lose state while scrollingInline
components
definitions or index-based keys
Hoist components; add
computeItemKey
List jumps on prependItems added without
firstItemIndex
adjustment
Use the
firstItemIndex
prepend pattern
"zero-sized element, this should not happen"An item rendered with zero heightFilter empty items from the data
"ResizeObserver loop completed with undelivered notifications" overlay in devBenign ResizeObserver timing, surfaced by the dev-server overlayDisable
runtimeErrors
in the webpack/vite overlay config; safe to filter in error tracking
Hard-to-explain size behavior—Set
logLevel={LogLevel.DEBUG}
and watch the console with all levels enabled
Tuning:
increaseViewportBy
renders extra pixels outside the viewport (smoother, more DOM);
defaultItemHeight
skips the initial probe render;
fixedItemHeight
skips measurement entirely (uniform items only);
scrollSeekConfiguration
swaps items for placeholders during fast scrolling.
症状可能原因修复方法
未渲染任何内容容器高度为零为组件或其父元素设置实际高度
无法滚动到最后几个项目/在底部附近跳动项目内容存在外边距将外边距替换为内边距
滚动时项目闪烁或丢失状态内联
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

参考资料

Detailed guides with full code in references/:
  • references/README.md — package overview and full feature list
  • references/1.virtuoso/
    — 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/2.grouped-virtuoso/
    — grouped lists: grouped-numbers, grouped-by-first-letter, scroll-to-group, grouped-with-load-on-demand
  • references/3.virtuoso-grid/
    — grid-responsive-columns
  • references/4.table-virtuoso/
    — tables: basic-table, table-fixed-headers, table-fixed-columns, table-grouped
  • references/5.third-party-integration/
    — mocking-in-tests, tanstack-table-integration, mui-table-virtual-scroll, material-ui-endless-scrolling
  • references/6.troubleshooting.md — full troubleshooting guide
包含完整代码的详细指南位于 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-mode
  • references/2.grouped-virtuoso/
    ——分组列表:grouped-numbers、grouped-by-first-letter、scroll-to-group、grouped-with-load-on-demand
  • references/3.virtuoso-grid/
    ——grid-responsive-columns
  • references/4.table-virtuoso/
    ——表格:basic-table、table-fixed-headers、table-fixed-columns、table-grouped
  • references/5.third-party-integration/
    ——mocking-in-tests、tanstack-table-integration、mui-table-virtual-scroll、material-ui-endless-scrolling
  • references/6.troubleshooting.md——完整故障排查指南