useVirtualList

Efficiently renders only the visible items in a long list using scroll position and item height.

✅ Features

  • Super fast performance for large lists
  • Avoids rendering off-screen elements
  • Smooth scrolling and customizable overscan buffer

📦 Usage

1const containerRef = useRef(null)
2
3const { list: visibleItems, totalHeight, scrollOffset } = useVirtualList(
4  items,
5  containerRef,
6  { itemHeight: 48 }
7)
8
9return (
10  <div ref={containerRef} className="h-[300px] overflow-auto relative">
11    <div style={{ height: totalHeight, position: 'relative' }}>
12      <div style={{ transform: `translateY(${scrollOffset}px)` }}>
13        {visibleItems.map((item, i) => (
14          <div key={i} className="h-[48px]">{item}</div>
15        ))}
16      </div>
17    </div>
18  </div>
19)

📋 Source Code

This is the full implementation of useVirtualList

1import { useEffect, useMemo, useRef, useState } from 'react'
2
3export interface UseVirtualListOptions {
4  itemHeight: number
5  overscan?: number
6}
7
8export function useVirtualList<T>(
9  list: T[],
10  containerRef: React.RefObject<HTMLElement>,
11  { itemHeight, overscan = 5 }: UseVirtualListOptions
12) {
13  const [scrollTop, setScrollTop] = useState(0)
14
15  const onScroll = () => {
16    if (containerRef.current) {
17      setScrollTop(containerRef.current.scrollTop)
18    }
19  }
20
21  useEffect(() => {
22    const ref = containerRef.current
23    if (!ref) return
24
25    ref.addEventListener('scroll', onScroll)
26    return () => ref.removeEventListener('scroll', onScroll)
27  }, [])
28
29  const totalHeight = list.length * itemHeight
30  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan)
31  const endIndex = Math.min(list.length - 1, Math.ceil((scrollTop + (containerRef.current?.clientHeight || 0)) / itemHeight) + overscan)
32
33  const visibleData = useMemo(() => list.slice(startIndex, endIndex + 1), [startIndex, endIndex, list])
34
35  return {
36    list: visibleData,
37    totalHeight,
38    scrollOffset: startIndex * itemHeight,
39  }
40}
41