@kookapp/virtual-layout-engine-react
v0.0.1
Published
React hooks and components for @kookapp/virtual-layout-engine
Readme
@kookapp/virtual-layout-engine-react
React hooks for @kookapp/virtual-layout-engine.
Installation
pnpm add @kookapp/virtual-layout-engine-react @kookapp/virtual-layout-enginePeer Dependencies
This package requires React 18+ to be installed in your project:
pnpm add react@^18.0.0 react-dom@^18.0.0Usage
useVirtualScroll
Core hook for virtual scrolling. Provides maximum flexibility and control.
import { DomPaddingRenderer, FixedSizeListModel, StaticDataProvider } from '@kookapp/virtual-layout-engine'
import { useVirtualScroll } from '@kookapp/virtual-layout-engine-react'
function MyVirtualList() {
const containerRef = useRef<HTMLDivElement>(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 (
<div
ref={containerRef}
style={{
height: '500px',
overflow: 'auto',
border: '1px solid #ccc',
}}
>
{!visibleRange && <div>Loading...</div>}
</div>
)
}useVirtualList
Convenient hook for fixed-size lists. Handles DataProvider and LayoutModel setup automatically.
import { DomPaddingRenderer } from '@kookapp/virtual-layout-engine'
import { useVirtualList } from '@kookapp/virtual-layout-engine-react'
function FixedSizeList() {
const containerRef = useRef<HTMLDivElement>(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 <div ref={containerRef} style={{ height: '500px', overflow: 'auto', border: '1px solid #ccc' }} />
}useDynamicVirtualList
Convenient hook for dynamic-size lists. Supports estimated sizes and automatic measurement.
import { DomPaddingRenderer } from '@kookapp/virtual-layout-engine'
import { useDynamicVirtualList } from '@kookapp/virtual-layout-engine-react'
function DynamicList() {
const containerRef = useRef<HTMLDivElement>(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 = `<div>Message ${index}</div>`
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 <div ref={containerRef} style={{ height: '500px', overflow: 'auto', border: '1px solid #ccc' }} />
}useAsyncFixedList
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
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<HTMLDivElement>(null)
const { visibleItems, paddingTop, paddingBottom, itemsContainerRef, isLoading } = useAsyncFixedList({
containerRef,
dataProvider,
itemSize: 80,
})
return (
<div ref={containerRef} style={{ height: '100vh', overflow: 'auto' }}>
<div style={{ height: paddingTop }} />
<div ref={itemsContainerRef}>
{visibleItems.map((item) => (
<div key={item.id} data-virtual-id={item.id} style={{ height: 80 }}>
{item.isLoaded && item.data ? (
<div>{item.data.title}</div>
) : (
<div className="skeleton">Loading...</div>
)}
</div>
))}
</div>
<div style={{ height: paddingBottom }} />
{isLoading && <div>Loading more...</div>}
</div>
)
}AsyncFixedList
Component for async paginated fixed-size lists. Built on top of useAsyncFixedList.
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 (
<AsyncFixedList
dataProvider={dataProvider}
itemSize={80}
renderItem={(data, index, isLoaded) => {
if (!isLoaded || !data) return null // Use default skeleton
return <div>{data.title}</div>
}}
renderLoading={() => <div>Loading more...</div>}
style={{ height: '100vh' }}
/>
)
}API
useVirtualScroll
Core hook for virtual scrolling.
const {
visibleRange,
scrollToId,
scrollToIndex,
scrollToOffset,
smoothScrollToId,
smoothScrollToIndex,
smoothScrollToOffset,
measureItems,
getVisibleRange,
isReady,
} = useVirtualScroll({
containerRef,
dataProvider,
layoutModel,
renderer,
defaultEstimatedSize,
overscan,
anchorStrategy,
onVisibleRangeChange,
onScroll,
})Parameters
containerRef: RefObject<HTMLElement | null> - Container element refdataProvider: IDataProvider - Data provider instancelayoutModel: IUILayoutModel - Layout model instancerenderer: IVirtualRenderer - Renderer instancedefaultEstimatedSize: number - Default estimated size for itemsoverscan?: number - Buffer size in pixels (default: 200)anchorStrategy?: IAnchorStrategy - Anchor strategy for scroll stabilityonVisibleRangeChange?: (result: VirtualLayoutResult) => void - Callback when visible range changesonScroll?: (offset: number) => void - Callback on scroll
Returns
visibleRange: VirtualLayoutResult | null - Current visible rangescrollToId: (id: VS_ID, align?: ScrollAlign) => void - Scroll to specific IDscrollToIndex: (index: number, align?: ScrollAlign) => void - Scroll to specific indexscrollToOffset: (offset: number) => void - Scroll to specific offsetsmoothScrollToId: (id: VS_ID, align?: ScrollAlign) => void - Smooth scroll to IDsmoothScrollToIndex: (index: number, align?: ScrollAlign) => void - Smooth scroll to indexsmoothScrollToOffset: (offset: number) => void - Smooth scroll to offsetmeasureItems: (ids?: VS_ID[]) => void - Trigger manual measurementgetVisibleRange: () => VirtualLayoutResult | null - Get current visible rangeisReady: boolean - Whether driver is ready
useVirtualList
Convenient hook for fixed-size lists.
const { visibleRange, scrollToId, ... } = useVirtualList({
containerRef,
ids: string[],
dataMap?: Map<string, T>,
itemSize: number,
overscan?: number,
onVisibleRangeChange?,
onScroll?,
})Parameters:
containerRef: Container element refids: Array of item IDsdataMap: Optional data mappingitemSize: Fixed item size in pixelsoverscan: Buffer size (default: 200)onVisibleRangeChange: Callback when visible range changesonScroll: Callback on scroll
Returns: Same as useVirtualScroll
useDynamicVirtualList
Convenient hook for dynamic-size lists.
const { visibleRange, scrollToId, ... } = useDynamicVirtualList({
containerRef,
ids: string[],
dataMap?: Map<string, T>,
defaultEstimatedSize: number,
estimatedSize?: (id: string | number) => number | null,
overscan?: number,
onVisibleRangeChange?,
onScroll?,
})Parameters:
containerRef: Container element refids: Array of item IDsdataMap: Optional data mappingdefaultEstimatedSize: Default estimated sizeestimatedSize: Optional function to estimate size based on IDoverscan: Buffer size (default: 300)onVisibleRangeChange: Callback when visible range changesonScroll: Callback on scroll
Returns: Same as useVirtualScroll
useAsyncFixedList
Hook for async paginated fixed-size lists.
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 refdataProvider: AsyncDataProvider instance (must be stable, created with useMemo)itemSize: Fixed item height in pixelsoverscan: Buffer size (default: 500)onLoadStart: Callback when data loading startsonLoadEnd: Callback when data loading ends (with success status)onVisibleRangeChange: Callback when visible range changesonScroll: Callback on scroll
Returns:
visibleItems: Array of visible items with{ id, index, offset, size, isLoaded, data }paddingTop: Top padding in pixelspaddingBottom: Bottom padding in pixelsitemsContainerRef: Ref for items container (for measurement)isLoading: Whether data is currently loadingisReady: Whether driver is initializedscrollToId: Scroll to specific IDscrollToIndex: Scroll to specific indexscrollToOffset: Scroll to specific offsetsmoothScrollToId: Smooth scroll to IDsmoothScrollToIndex: Smooth scroll to indexsmoothScrollToOffset: Smooth scroll to offsetloadMore: Manually trigger data loadingupdateItemSizes: Update item sizes after measurement
AsyncFixedList
Component for async paginated fixed-size lists.
<AsyncFixedList
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:
pnpm testTests are located in the tests/ directory:
useVirtualScroll.test.ts- Core hook testsuseVirtualList.test.ts- Fixed-size hook testsuseDynamicVirtualList.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
Running Demos
Simply open any demo HTML file in your browser:
# On macOS
open demo/fixed-size-list.html
# On Windows
start demo/fixed-size-list.html
# On Linux
xdg-open demo/fixed-size-list.htmlLicense
ISC
