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