Rendering 100,000 rows into the DOM will freeze the browser — but the user only ever sees a dozen at a time. Virtualization (windowing) renders only the rows currently in the viewport (plus a small buffer), positioned so the scrollbar still behaves as if all rows existed. useVirtualList does the math: given the item count, row height, and viewport height, it tells you which rows to render and where to place each one.
Implement useVirtualList({ itemCount, itemHeight, containerHeight, overscan = 3 }). Return virtualItems (the visible rows with their absolute offsets), totalHeight (so the scroll area is the right size), startIndex/endIndex, and an onScroll handler that tracks the container's scrollTop.
function useVirtualList({ itemCount, itemHeight, containerHeight, overscan = 3 }) {
// returns { virtualItems, totalHeight, startIndex, endIndex, scrollTop, onScroll }
}
const { virtualItems, totalHeight, onScroll } =
useVirtualList({ itemCount: rows.length, itemHeight: 32, containerHeight: 400 });
<div style={{ height: 400, overflow: 'auto' }} onScroll={onScroll}>
<div style={{ height: totalHeight, position: 'relative' }}>
{virtualItems.map(({ index, offsetTop }) => (
<Row key={index} style={{ position: 'absolute', top: offsetTop }} {...rows[index]} />
))}
</div>
</div>
itemCount * itemHeight, so the scroll thumb reflects the full list even though most rows aren't rendered.scrollTop and containerHeight: start = floor(scrollTop / itemHeight), end = floor((scrollTop + containerHeight) / itemHeight).start >= 0 and end <= itemCount - 1.offsetTop = index * itemHeight positions it correctly inside the full-height spacer.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.