@dysonic/virtual-list
v1.1.0
Published
A lightweight React virtual list component powered by `IntersectionObserver`.
Readme
@dysonic/virtual-list
A lightweight React virtual list component powered by IntersectionObserver.
Unlike index-calculation-based virtualization libraries, this component is designed to work well with dynamic item heights. It renders visible items, keeps placeholders for offscreen items, and uses measured heights to keep the list stable while scrolling.
Features
- Lightweight implementation based on
IntersectionObserver - Works with dynamic-height content
- Supports prerender buffers for smoother scrolling
- Supports
rowKeyas either a field name or a function - React 16+ compatible
Why not react-window?
react-window is a great choice when item heights are fixed or can be calculated ahead of time. This library takes a different approach:
react-windowmainly relies on index-based range calculation, while@dysonic/virtual-listrelies onIntersectionObserverreact-windowis best suited to fixed-size or carefully managed variable-size lists, while this component is friendlier to naturally dynamic content- this component measures rendered items and reuses their heights for placeholders, so mixed-height content can scroll more smoothly with less manual bookkeeping
- the API stays small and focuses on a single list component rather than multiple layout primitives
If your list items have stable, known sizes, react-window may still be a better fit. If your list contains content with changing or hard-to-predict heights, this library is the more convenient option.
Installation
pnpm add @dysonic/virtual-listYou also need compatible peer dependencies:
pnpm add react react-domDevelopment
All commands are intended to run from the repository root:
pnpm install
pnpm build
pnpm test
pnpm docs:devRelease (Maintainers)
Set the version:
pnpm exec dy-cli version --set 1.2.3Publish:
pnpm exec dy-cli publishDemo
This package now includes a docs site built with dumi, following the package-local docs organization used by Ant Design:
- 200 fixed-height rows
- 200 dynamic-height rows with naturally changing content height
Run it locally:
pnpm docs:devBuild the docs site:
pnpm docs:buildThe docs source lives in docs.
Quick Start
import React from 'react';
import { VirtualList } from '@dysonic/virtual-list';
type User = {
id: string;
name: string;
bio: string;
};
const users: User[] = [
{ id: '1', name: 'Ada', bio: 'Short bio' },
{ id: '2', name: 'Grace', bio: 'A much longer bio that makes this row taller than the others.' },
];
export function Demo() {
return (
<VirtualList<User>
data={users}
rowKey="id"
prerender
style={{ height: 480, overflowY: 'auto' }}
render={(user) => (
<article style={{ padding: 12, borderBottom: '1px solid #eee' }}>
<h4>{user.name}</h4>
<p>{user.bio}</p>
</article>
)}
/>
);
}Docs Examples
Demo Example: Fixed Height (200 Rows)
import React from 'react';
import { VirtualList } from '@dysonic/virtual-list';
const orders = Array.from({ length: 200 }, (_, index) => ({
id: `fixed-${index + 1}`,
title: `Order #${1000 + index + 1}`,
subtitle: `Fixed-height row ${index + 1}`,
}));
export function FixedHeightDemo() {
return (
<VirtualList
data={orders}
rowKey="id"
prerender
minHeightOnHolderItem={72}
style={{ height: 420, overflowY: 'auto' }}
render={(item) => (
<article style={{ display: 'flex', justifyContent: 'space-between', padding: 16 }}>
<div>
<strong>{item.title}</strong>
<p>{item.subtitle}</p>
</div>
</article>
)}
/>
);
}Demo Example: Dynamic Height (200 Rows)
import React from 'react';
import { VirtualList } from '@dysonic/virtual-list';
const messages = Array.from({ length: 200 }, (_, index) => ({
id: `dynamic-${index + 1}`,
title: `Conversation ${index + 1}`,
paragraphs: [
'Short content.',
'A second paragraph makes this row taller.',
'A third paragraph is useful when you want more obvious height differences.',
'A fourth paragraph exaggerates the dynamic-height effect for the demo.',
].slice(0, (index % 4) + 1),
}));
export function DynamicHeightDemo() {
return (
<VirtualList
data={messages}
rowKey="id"
prerender={4}
minHeightOnHolderItem={96}
style={{ height: 420, overflowY: 'auto' }}
render={(item) => (
<article style={{ padding: 16, borderBottom: '1px solid #eee' }}>
<strong>{item.title}</strong>
{item.paragraphs.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</article>
)}
/>
);
}How it Works
The component renders real items for entries that are currently intersecting the viewport. Items outside the viewport are replaced with placeholder elements. Once an item has been rendered, its measured height is cached and reused by the placeholder, which helps maintain scroll continuity even when item heights are different.
When prerender is enabled, the component also keeps a small buffer of nearby items mounted before and after the visible range to reduce flicker during fast scrolling.
API
VirtualList<T>
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| data | T[] | - | The full list of items to render |
| rowKey | keyof T \| (item: T) => string | - | Unique key for each item |
| render | (item: T, index: number) => React.ReactNode | - | Render function for each list item |
| prerender | boolean \| number | false | Enables prerendering. When a number is provided, it is used as the buffer size |
| prerenderBufferSize | number | 2 | Buffer size used when prerender={true} |
| maxHolderItems | number | 50 | Maximum number of placeholder items to keep in the DOM when prerendering |
| minHeightOnHolderItem | number | 150 | Fallback placeholder height before an item has been measured |
| className | string | - | Class name applied to the outer wrapper |
| holderItemClassName | string | - | Class name applied to placeholder items |
| style | React.CSSProperties | - | Inline styles applied to the outer wrapper |
Recommended Usage
- Wrap the list in a scrollable container with an explicit height
- Use a stable
rowKey - Enable
prerenderwhen users may scroll quickly - Tune
minHeightOnHolderItemwhen your average item height is known - Increase
maxHolderItemsif you notice visible height jumps during fast scrolling
Notes
- The component depends on
IntersectionObserver. For older environments, include a polyfill if needed - Dynamic-height support works best when each item eventually renders to a stable height
- Very inaccurate
minHeightOnHolderItemvalues may cause temporary scroll jumps before enough items have been measured
License
MIT
