Virtual List

Renders only the rows in view. DataGrid and LogViewer both render every row they are given, which is fine until a few thousand and then stalls the main thread.

src/components/ui/virtual-list.tsx

About

From the component's own source, where it was written.

Renders only the rows in view.

DataGrid and LogViewer both render every row they are given, which is fine until a few thousand and then stalls the main thread on every update. This is the layer they can sit on.

Fixed row height by design. Variable heights need measurement, a cache and a correction pass for the scroll position when an estimate turns out wrong — an order of magnitude more code, and wrong estimates make the scrollbar jump under the user's hand. A fixed height keeps the maths exact.

overscan renders a few rows beyond the viewport so a fast scroll does not expose blank space before the next paint.

Install

npx astralyx-ui add virtual-list

Usage

import { VirtualList } from '@/components/ui/virtual-list'

<VirtualList
  items={rows}
  rowHeight={36}
  renderItem={(row) => <Row row={row} />}
  onEndReached={loadMore}
/>

Composer

Change the props and the source below follows.

1Row 10
2Row 237
3Row 374
4Row 4111
5Row 5148
6Row 6185
7Row 7222
8Row 8259
Props
tsx
<VirtualList
  items={rows}
  rowHeight={36}
  height={280}
  renderItem={(row) => <Row row={row} />}
/>

Examples

5,000 rows

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

VirtualList<Item>

Renders only the rows in view.

Extends Omit<ComponentProps<'div'>, 'children' | 'height'>.

PropTypeDefaultDescription
itemsItem[]requiredFixed height by design: variable heights need measurement, a cache and a scroll correction pass, and wrong estimates make the scrollbar jump under the pointer.
rowHeightnumberrequiredFixed height by design: variable heights need measurement, a cache and a scroll correction pass, and wrong estimates make the scrollbar jump under the pointer.
renderItem(item: Item, index: number) => ReactNoderequiredThe index is the real one in the full list, not the slice.
overscannumber4Rows rendered beyond the viewport so a fast scroll does not expose blank space.
heightnumber | string400—
onEndReached() => void—Infinite scrolling. Guarded by item count, so it fires once per batch rather than on every scroll event near the bottom.
endThresholdnumber200—