npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-engine

Peer Dependencies

This package requires React 18+ to be installed in your project:

pnpm add react@^18.0.0 react-dom@^18.0.0

Usage

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 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

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 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

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 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

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 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

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:

Testing

Run tests with:

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:

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.html

License

ISC