React hooks and components for @kookapp/virtual-layout-engine
npm install @kookapp/virtual-layout-engine-reactbash
pnpm add @kookapp/virtual-layout-engine-react @kookapp/virtual-layout-engine
`
Peer Dependencies
This package requires React 18+ to be installed in your project:
`bash
pnpm add react@^18.0.0 react-dom@^18.0.0
`
Usage
$3
Core hook for virtual scrolling. Provides maximum flexibility and control.
`tsx
import { DomPaddingRenderer, FixedSizeListModel, StaticDataProvider } from '@kookapp/virtual-layout-engine'
import { useVirtualScroll } from '@kookapp/virtual-layout-engine-react'
function MyVirtualList() {
const containerRef = useRef(null)
// Prepare data
const ids = Array.from({ length: 10000 }, (_, i) => item-${i})
// Create data provider
const dataProvider = useMemo(
() =>
new StaticDataProvider({
ids,
}),
[ids]
)
// Create layout model
const layoutModel = useMemo(
() =>
new FixedSizeListModel({
itemSize: 50,
totalLength: ids.length,
}),
[ids.length]
)
// Create renderer
const renderer = useMemo(() => {
if (!containerRef.current) return null
return new DomPaddingRenderer({
container: containerRef.current,
renderItem: (id, data, index) => {
const div = document.createElement('div')
div.style.height = '50px'
div.style.padding = '10px'
div.style.borderBottom = '1px solid #eee'
div.textContent = Item ${index}: ${id}
return div
},
})
}, [])
// Use virtual scroll hook
const { visibleRange, scrollToId } = useVirtualScroll({
containerRef,
dataProvider,
layoutModel: layoutModel!,
renderer: renderer!,
defaultEstimatedSize: 50,
overscan: 200,
onVisibleRangeChange: (result) => {
console.log('Visible items:', result.visibleItems.length)
},
})
return (
ref={containerRef}
style={{
height: '500px',
overflow: 'auto',
border: '1px solid #ccc',
}}
>
{!visibleRange && Loading...}
$3
Convenient hook for fixed-size lists. Handles DataProvider and LayoutModel setup automatically.
`tsx
import { DomPaddingRenderer } from '@kookapp/virtual-layout-engine'
import { useVirtualList } from '@kookapp/virtual-layout-engine-react'
function FixedSizeList() {
const containerRef = useRef(null)
const ids = Array.from({ length: 10000 }, (_, i) => item-${i})
const renderer = useMemo(() => {
if (!containerRef.current) return null
return new DomPaddingRenderer({
container: containerRef.current,
renderItem: (id, data, index) => {
const div = document.createElement('div')
div.style.height = '50px'
div.style.padding = '10px'
div.textContent = Item ${index}
return div
},
})
}, [])
const { visibleRange, scrollToId } = useVirtualList({
containerRef,
ids,
itemSize: 50,
renderer: renderer!,
})
return
}
`
$3
Convenient hook for dynamic-size lists. Supports estimated sizes and automatic measurement.
`tsx
import { DomPaddingRenderer } from '@kookapp/virtual-layout-engine'
import { useDynamicVirtualList } from '@kookapp/virtual-layout-engine-react'
function DynamicList() {
const containerRef = useRef(null)
const ids = Array.from({ length: 1000 }, (_, i) => msg-${i})
const renderer = useMemo(() => {
if (!containerRef.current) return null
return new DomPaddingRenderer({
container: containerRef.current,
renderItem: (id, data, index) => {
const div = document.createElement('div')
div.style.padding = '12px'
div.style.borderBottom = '1px solid #f0f0f0'
div.style.height = ${60 + (index % 10) * 20}px // Variable height
div.innerHTML =
return div
},
batchMeasure: true,
})
}, [])
const { visibleRange } = useDynamicVirtualList({
containerRef,
ids,
defaultEstimatedSize: 80,
estimatedSize: (id) => {
const index = parseInt(String(id).split('-')[1])
return 60 + (index % 10) * 20
},
renderer: renderer!,
overscan: 300,
})
return
}
`
$3
Hook for async paginated fixed-size lists. Perfect for infinite scrolling with API data loading.
Key Features:
- Async data loading with pagination
- Automatic skeleton screen support
- Stable provider instance (mutable internal state)
- Single driver initialization (no rebuilds)
- Event-driven data synchronization
`tsx
import { AsyncDataProvider } from '@kookapp/virtual-layout-engine'
import { useAsyncFixedList } from '@kookapp/virtual-layout-engine-react'
function AsyncList() {
// Create AsyncDataProvider (keep stable with useMemo)
const dataProvider = useMemo(
() =>
new AsyncDataProvider({
totalCount: 10000, // Total item count
loadData: async (start, count) => {
const res = await fetch(/api/items?start=${start}&count=${count})
const items = await res.json()
return items.map((item) => ({ id: item.id, data: item }))
},
pageSize: 50, // Load 50 items at a time
}),
[]
)
const containerRef = useRef(null)
const { visibleItems, paddingTop, paddingBottom, itemsContainerRef, isLoading } = useAsyncFixedList({
containerRef,
dataProvider,
itemSize: 80,
})
return (
{visibleItems.map((item) => (
{item.isLoaded && item.data ? (
{item.data.title}
) : (
Loading...
)}
))}
{isLoading && Loading more...}
)
}
`
$3
Component for async paginated fixed-size lists. Built on top of useAsyncFixedList.
`tsx
import { AsyncDataProvider } from '@kookapp/virtual-layout-engine'
import { AsyncFixedList } from '@kookapp/virtual-layout-engine-react'
function MyList() {
const dataProvider = useMemo(
() =>
new AsyncDataProvider({
totalCount: 10000,
loadData: async (start, count) => {
const res = await fetch(/api/items?start=${start}&count=${count})
const items = await res.json()
return items.map((item) => ({ id: item.id, data: item }))
},
pageSize: 50,
}),
[]
)
return (
dataProvider={dataProvider}
itemSize={80}
renderItem={(data, index, isLoaded) => {
if (!isLoaded || !data) return null // Use default skeleton
return {data.title}
}}
renderLoading={() => Loading more...}
style={{ height: '100vh' }}
/>
)
}
`
API
$3
Core hook for virtual scrolling.
`tsx
const {
visibleRange,
scrollToId,
scrollToIndex,
scrollToOffset,
smoothScrollToId,
smoothScrollToIndex,
smoothScrollToOffset,
measureItems,
getVisibleRange,
isReady,
} = useVirtualScroll({
containerRef,
dataProvider,
layoutModel,
renderer,
defaultEstimatedSize,
overscan,
anchorStrategy,
onVisibleRangeChange,
onScroll,
})
`
#### Parameters
- containerRef: RefObject - Container element ref
- dataProvider: IDataProvider - Data provider instance
- layoutModel: IUILayoutModel - Layout model instance
- renderer: IVirtualRenderer - Renderer instance
- defaultEstimatedSize: number - Default estimated size for items
- overscan?: number - Buffer size in pixels (default: 200)
- anchorStrategy?: IAnchorStrategy - Anchor strategy for scroll stability
- onVisibleRangeChange?: (result: VirtualLayoutResult) => void - Callback when visible range changes
- onScroll?: (offset: number) => void - Callback on scroll
#### Returns
- visibleRange: VirtualLayoutResult | null - Current visible range
- scrollToId: (id: VS_ID, align?: ScrollAlign) => void - Scroll to specific ID
- scrollToIndex: (index: number, align?: ScrollAlign) => void - Scroll to specific index
- scrollToOffset: (offset: number) => void - Scroll to specific offset
- smoothScrollToId: (id: VS_ID, align?: ScrollAlign) => void - Smooth scroll to ID
- smoothScrollToIndex: (index: number, align?: ScrollAlign) => void - Smooth scroll to index
- smoothScrollToOffset: (offset: number) => void - Smooth scroll to offset
- measureItems: (ids?: VS_ID[]) => void - Trigger manual measurement
- getVisibleRange: () => VirtualLayoutResult | null - Get current visible range
- isReady: boolean - Whether driver is ready
$3
Convenient hook for fixed-size lists.
`tsx
const { visibleRange, scrollToId, ... } = useVirtualList({
containerRef,
ids: string[],
dataMap?: Map,
itemSize: number,
overscan?: number,
onVisibleRangeChange?,
onScroll?,
})
`
Parameters:
- containerRef: Container element ref
- ids: Array of item IDs
- dataMap: Optional data mapping
- itemSize: Fixed item size in pixels
- overscan: Buffer size (default: 200)
- onVisibleRangeChange: Callback when visible range changes
- onScroll: Callback on scroll
Returns: Same as useVirtualScroll
$3
Convenient hook for dynamic-size lists.
`tsx
const { visibleRange, scrollToId, ... } = useDynamicVirtualList({
containerRef,
ids: string[],
dataMap?: Map,
defaultEstimatedSize: number,
estimatedSize?: (id: string | number) => number | null,
overscan?: number,
onVisibleRangeChange?,
onScroll?,
})
`
Parameters:
- containerRef: Container element ref
- ids: Array of item IDs
- dataMap: Optional data mapping
- defaultEstimatedSize: Default estimated size
- estimatedSize: Optional function to estimate size based on ID
- overscan: Buffer size (default: 300)
- onVisibleRangeChange: Callback when visible range changes
- onScroll: Callback on scroll
Returns: Same as useVirtualScroll
$3
Hook for async paginated fixed-size lists.
`tsx
const {
visibleItems,
paddingTop,
paddingBottom,
itemsContainerRef,
isLoading,
isReady,
scrollToId,
scrollToIndex,
scrollToOffset,
smoothScrollToId,
smoothScrollToIndex,
smoothScrollToOffset,
loadMore,
updateItemSizes,
} = useAsyncFixedList({
containerRef,
dataProvider,
itemSize,
overscan,
onLoadStart,
onLoadEnd,
onVisibleRangeChange,
onScroll,
})
`
Parameters:
- containerRef: Container element ref
- dataProvider: AsyncDataProvider instance (must be stable, created with useMemo)
- itemSize: Fixed item height in pixels
- overscan: Buffer size (default: 500)
- onLoadStart: Callback when data loading starts
- onLoadEnd: Callback when data loading ends (with success status)
- onVisibleRangeChange: Callback when visible range changes
- onScroll: Callback on scroll
Returns:
- visibleItems: Array of visible items with { id, index, offset, size, isLoaded, data }
- paddingTop: Top padding in pixels
- paddingBottom: Bottom padding in pixels
- itemsContainerRef: Ref for items container (for measurement)
- isLoading: Whether data is currently loading
- isReady: Whether driver is initialized
- scrollToId: Scroll to specific ID
- scrollToIndex: Scroll to specific index
- scrollToOffset: Scroll to specific offset
- smoothScrollToId: Smooth scroll to ID
- smoothScrollToIndex: Smooth scroll to index
- smoothScrollToOffset: Smooth scroll to offset
- loadMore: Manually trigger data loading
- updateItemSizes: Update item sizes after measurement
$3
Component for async paginated fixed-size lists.
`tsx
dataProvider={dataProvider}
itemSize={number}
renderItem={(data, index, isLoaded) => ReactNode}
renderSkeleton?={(index) => ReactNode}
renderLoading?={() => ReactNode}
renderEmpty?={() => ReactNode}
style?={CSSProperties}
className?={string}
overscan?={number}
onLoadStart?={() => void}
onLoadEnd?={(success: boolean) => void}
onVisibleRangeChange?={(result) => void}
onScroll?={(offset: number) => void}
ref?={AsyncFixedListRef}
/>
`
Props:
- dataProvider: AsyncDataProvider instance (required)
- itemSize: Fixed item height in pixels (required)
- renderItem: Function to render each item (required)
- renderSkeleton: Function to render skeleton (optional, has default)
- renderLoading: Function to render loading indicator (optional, has default)
- renderEmpty: Function to render empty state (optional)
- style: Container style (optional)
- className: Container class name (optional)
- overscan: Buffer size (default: 500)
- Event callbacks (same as useAsyncFixedList)
Ref Methods:
- scrollToId(id, align?)
- scrollToIndex(index, align?)
- scrollToOffset(offset)
- smoothScrollToId(id, align?)
- smoothScrollToIndex(index, align?)
- smoothScrollToOffset(offset)
- loadMore(): Manually trigger loading
Examples
See complete examples in:
- Basic fixed-size list
- Dynamic-size list with estimation
- Async paginated list
- Custom implementation with useVirtualScroll
- Interactive demo - Full working example with both Hook and Component usage
Testing
Run tests with:
`bash
pnpm test
`
Tests are located in the tests/ directory:
- useVirtualScroll.test.ts - Core hook tests
- useVirtualList.test.ts - Fixed-size hook tests
- useDynamicVirtualList.test.ts - Dynamic-size hook tests
Demos
Interactive demos are available in the demo/ directory:
- fixed-size-list.html - Fixed height list with 10,000 items
- dynamic-size-list.html - Variable height chat messages with 1,000 items
$3
Simply open any demo HTML file in your browser:
`bash
On macOS
open demo/fixed-size-list.html
On Windows
start demo/fixed-size-list.html
On Linux
xdg-open demo/fixed-size-list.html
``