@radhan000123/use-virtual-list
v1.0.3
Published
A high-performance React hook for rendering virtual lists.
Maintainers
Readme
@radhan000123/use-virtual-list
A high-performance React hook for rendering virtual lists. It renders only the items currently visible in the DOM (plus a small buffer), making it highly efficient for rendering massive lists or grids.
Website
Installation
npm install @radhan000123/use-virtual-listFeatures
- High Performance: Renders only visible items.
- Buffer Support: Configurable buffer size to prevent flickering during fast scrolling.
- Grid Setup: Easily supports multiple items per row (
itemsPerRow). - TypeScript: Written in TypeScript with full type definitions.
Usage
Explore different use cases for implementing the virtual list hook.
1. Single Column (Basic)
The most common use case for rendering a simple, single-column vertical list.
import React, { useRef, useMemo } from 'react';
import useVirtualList from '@radhan000123/use-virtual-list';
const SingleColumnList = () => {
const allItems = useMemo(() => Array.from({ length: 100000 }, (_, i) => `Item ${i + 1}`), []);
const outerContainerRef = useRef<HTMLDivElement>(null);
const {
handleOuterContainerScroll,
visibleRows,
slidingWindowTranslatePx,
innerContainerHeight,
} = useVirtualList({
allItems,
rowHeightPx: 50,
itemsPerRow: 1, // Rendering 1 item per row
outerContainerRef,
buffer: 5,
});
return (
<div
ref={outerContainerRef}
onScroll={handleOuterContainerScroll}
style={{ height: '500px', overflow: 'auto' }}
>
<div style={{ height: `${innerContainerHeight}px`, position: 'relative' }}>
<div style={{ transform: `translateY(${slidingWindowTranslatePx}px)` }}>
{visibleRows.map((row, rowIndex) => (
<div key={rowIndex} style={{ height: '50px' }}>
{/* row[0] contains our single item */}
{row[0]}
</div>
))}
</div>
</div>
</div>
);
};2. Responsive Multi-Column (Grid)
Calculate and pass the itemsPerRow dynamically by observing your container's width. Great for masonry or grid layouts.
import React, { useRef, useState, useEffect, useMemo } from 'react';
import useVirtualList from '@radhan000123/use-virtual-list';
const MultiColumnGrid = () => {
const allItems = useMemo(() => Array.from({ length: 10000 }, (_, i) => ({ id: i, label: `Card ${i}` })), []);
const outerContainerRef = useRef<HTMLDivElement>(null);
const [itemsPerRow, setItemsPerRow] = useState(1);
const MIN_COL_WIDTH = 200;
// Dynamically calculate columns based on container width
useEffect(() => {
if (!outerContainerRef.current) return;
const observer = new ResizeObserver((entries) => {
const width = entries[0].contentRect.width;
setItemsPerRow(Math.max(1, Math.floor(width / MIN_COL_WIDTH)));
});
observer.observe(outerContainerRef.current);
return () => observer.disconnect();
}, []);
const {
handleOuterContainerScroll,
visibleRows,
slidingWindowTranslatePx,
innerContainerHeight,
} = useVirtualList({
allItems,
rowHeightPx: 150, // Height of the grid row
itemsPerRow,
outerContainerRef,
buffer: 3,
});
return (
<div ref={outerContainerRef} onScroll={handleOuterContainerScroll} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${innerContainerHeight}px`, position: 'relative' }}>
<div style={{ transform: `translateY(${slidingWindowTranslatePx}px)` }}>
{visibleRows.map((row, rowIndex) => (
<div key={rowIndex} style={{ display: 'flex', gap: '8px', height: '150px' }}>
{row.map(item => (
<div key={item.id} style={{ flex: 1, background: '#eee' }}>
{item.label}
</div>
))}
{/* Fill empty grid spaces if the last row is incomplete */}
{Array.from({ length: itemsPerRow - row.length }).map((_, i) => (
<div key={`empty-${i}`} style={{ flex: 1 }} />
))}
</div>
))}
</div>
</div>
</div>
);
};3. With Infinite Scroll (API Fetching)
Combine the virtual window scroll handling with your own infinite loading logic when reaching the bottom of the scroll container.
import React, { useRef, useState, useEffect } from 'react';
import type { UIEventHandler } from 'react';
import useVirtualList from '@radhan000123/use-virtual-list';
const InfiniteScrollList = () => {
const outerContainerRef = useRef<HTMLDivElement>(null);
const [items, setItems] = useState<any[]>([]);
const [offset, setOffset] = useState(0);
// Example API fetch simulator
const fetchMoreData = async () => {
const res = await fetch(`/api/items?offset=${offset}&limit=20`);
const data = await res.json();
setItems(prev => [...prev, ...data.items]);
setOffset(data.nextOffset);
};
useEffect(() => { fetchMoreData(); }, []);
const {
handleOuterContainerScroll,
visibleRows,
slidingWindowTranslatePx,
innerContainerHeight,
} = useVirtualList({
allItems: items,
rowHeightPx: 70,
itemsPerRow: 1,
outerContainerRef,
buffer: 3,
});
// Intercept the onScroll event to detect bottom reach
const onScroll: UIEventHandler<HTMLDivElement> = (e) => {
// 1. Maintain virtual list position updates
handleOuterContainerScroll(e);
// 2. Fetch more when hitting the bottom threshold
const target = e.currentTarget;
if (target.scrollHeight - target.scrollTop - target.clientHeight < 150) {
// Trigger fetch only if not currently loading
fetchMoreData();
}
};
return (
<div ref={outerContainerRef} onScroll={onScroll} style={{ height: '500px', overflow: 'auto' }}>
<div style={{ height: `${innerContainerHeight}px`, position: 'relative' }}>
<div style={{ transform: `translateY(${slidingWindowTranslatePx}px)` }}>
{visibleRows.map((row) => (
<div key={row[0].id} style={{ height: '70px', borderBottom: '1px solid #ccc' }}>
{row[0].title}
</div>
))}
</div>
</div>
</div>
);
};API
useVirtualList(params)
Parameters
allItems(T[]): Array of total items to render.rowHeightPx(number): Height of each row in pixels.itemsPerRow(number): Number of items in each row (useful for grids).outerContainerRef(RefObject): A React ref attached to the scrolling container element.buffer(number, optional): Number of extra rows to render above and below the visible area to prevent flickering. Default is5.
Returns
handleOuterContainerScroll(UIEventHandler): Event handler to attach to theonScrollevent of the outer container.visibleRows(T[][]): 2D array of items to render in the current viewport.slidingWindowTranslatePx(number): The pixel value fortranslateYto properly adjust the position.innerContainerHeight(number): Dynamic total height of the inner container to maintain appropriate scroll bounds.
License
MIT
