@lazy-virtual-scroll/react
v1.0.1
Published
A high-performance virtualized list component for React that efficiently renders large datasets with dynamic sizing and lazy loading
Maintainers
Readme
React Lazy Virtual Scroll
[](https://opensource.org/licenses/MIT)
A highly performant virtualized list component for React that efficiently renders large datasets with dynamic sizing, lazy loading, and bi-directional scrolling support.
Features
- Virtualized Rendering: Only renders the items currently visible in the viewport
- Dynamic Sizing: Automatically detects and handles items of varying heights
- Lazy Loading: Load data on-demand as the user scrolls
- Bi-directional Scrolling: Vertical and horizontal scrolling support
- Performance Optimized: Debounced and throttled scroll handling
- Flexible Data Structure: Support for continuous or fragmented datasets
- Typescript Support: Full type definitions included
Installation
# npm
npm install @lazy-virtual-scroll/react
# yarn
yarn add @lazy-virtual-scroll/react
# pnpm
pnpm add @lazy-virtual-scroll/reactBasic Usage
import React, { useState } from 'react';
import LazyVirtualScroll from '@lazy-virtual-scroll/react';
const MyList = () => {
const [items] = useState(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
})));
return (
<div style={{ height: '500px', width: '100%' }}>
<LazyVirtualScroll
totalItems={items.length}
itemSize={70}
data={items}
onLoad={({ startIndex, endIndex }) => {
console.log(`Loading items from ${startIndex} to ${endIndex}`);
}}
onHide={({ startIndex, endIndex }) => {
console.log(`Hiding items from ${startIndex} to ${endIndex}`);
}}
render={(index, item) => (
<div style={{
height: '50px',
padding: '10px',
borderBottom: '1px solid #eee',
boxSizing: 'border-box' // Ensures padding is included in height
}}>
{item.text}
</div>
)}
renderLoading={(index) => (
<div style={{
height: '50px',
padding: '10px',
borderBottom: '1px solid #eee',
backgroundColor: '#f5f5f5',
boxSizing: 'border-box' // Ensures padding is included in height
}}>
Loading item {index}...
</div>
)}
/>
</div>
);
};Advanced Example
import React, { useState } from 'react';
import LazyVirtualScroll from 'react-lazy-virtual-scroll';
const AdvancedExample = () => {
const [items] = useState(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`,
expanded: false
})));
const [expandedItems, setExpandedItems] = useState({});
const toggleExpand = (index) => {
setExpandedItems(prev => {
const newState = { ...prev };
if (newState[index]) {
delete newState[index];
} else {
newState[index] = 300; // expanded height
}
return newState;
});
};
return (
<div style={{ height: '500px', width: '100%' }}>
<LazyVirtualScroll
totalItems={items.length}
itemSize={50}
data={items}
autoDetectSizes={true}
dynamicSizes={expandedItems}
scrollDebounce={100}
direction="column"
onLoad={({ startIndex, endIndex }) => {
// You could fetch data here if needed
console.log(`Visible range: ${startIndex} - ${endIndex}`);
}}
onHide={({ startIndex, endIndex }) => {
// Clean up or unload data when items go out of view
console.log(`Hidden range: ${startIndex} - ${endIndex}`);
}}
render={(index, item) => (
<div
className={expandedItems[index] ? 'expanded-item' : 'item'}
onClick={() => toggleExpand(index)}
>
<div className="item-header">
{item.text}
<span>{expandedItems[index] ? '▲' : '▼'}</span>
</div>
{expandedItems[index] && (
<div className="item-content" style={{ height: `${expandedItems[index]}px` }}>
<p>Expanded content for item {index}</p>
</div>
)}
</div>
)}
renderLoading={(index) => (
<div className="loading-item">
Loading item {index}...
</div>
)}
/>
</div>
);
};Rendering
Item Rendering with render
The render prop is a required function that determines how each item is displayed:
<LazyVirtualScroll
// ...other props
render={(index, item) => (
<div className="list-item">
<h3>Item {index}</h3>
{item ? (
<p>{item.content}</p>
) : (
<p>Data not loaded yet</p>
)}
</div>
)}
/>Parameters:
index(number): The index of the item in the listitem(any): The data item from yourdataarray ordatasets. Will beundefinedif data hasn't been loaded yet.
Loading State with renderLoading
The optional renderLoading prop lets you customize the loading state for items that haven't been loaded yet:
<LazyVirtualScroll
// ...other props
renderLoading={(index) => (
<div className="loading-item">
<div className="spinner"></div>
<span>Loading item {index}...</span>
</div>
)}
/>Parameters:
index(number): The index of the loading item
If renderLoading is not provided, the render function will be called with item as undefined.
Callbacks
onLoad Callback
Called when new items become visible and need to be loaded:
const handleLoad = ({ startIndex, endIndex }) => {
console.log(`Need to load items from ${startIndex} to ${endIndex}`);
// Example: Fetch data for this range
fetchData(startIndex, endIndex).then(newData => {
setItems(prevItems => {
const updatedItems = [...prevItems];
newData.forEach((item, i) => {
updatedItems[startIndex + i] = item;
});
return updatedItems;
});
});
};
<LazyVirtualScroll onLoad={handleLoad} /* ...other props */ />onHide Callback
Called when items go out of view:
const handleHide = ({ startIndex, endIndex }) => {
console.log(`Items ${startIndex} to ${endIndex} are now hidden`);
// Example: Clean up resources or mark items for garbage collection
cleanupItems(startIndex, endIndex);
};
<LazyVirtualScroll onHide={handleHide} /* ...other props */ />onScroll Callback
Called when the user scrolls:
const handleScroll = (scrollPosition) => {
console.log(`Current scroll position: ${scrollPosition}px`);
// Example: Update URL or save scroll position
updateScrollPosition(scrollPosition);
};
<LazyVirtualScroll onScroll={handleScroll} /* ...other props */ />Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| totalItems | number | (required) | Total number of items in the list |
| itemSize | number | (required) | Base height/width of each item in pixels |
| data | any[] | [] | Array of data items to render |
| datasets | Dataset[] | [] | Alternative to data for fragmented datasets |
| render | (index: number, item: any) => ReactNode | (required) | Function to render each item |
| renderLoading | (index: number) => ReactNode | undefined | Function to render loading state |
| direction | 'row' \| 'column' | 'column' | Scroll direction |
| itemBuffer | number | 3 | Number of items to render outside visible area |
| scrollThrottle | number | 0 | Throttle scroll events (milliseconds) |
| scrollDebounce | number | 0 | Debounce scroll events (milliseconds) |
| scrollStart | number | 0 | Initial scroll position |
| dynamicSizes | { [itemIndex: string]: number } | {} | Manual size overrides for specific items |
| autoDetectSizes | boolean | false | Automatically detect item sizes |
| minItemSize | number | 0 | Minimum size for dynamically sized items |
| sortDatasets | boolean | true | Automatically sort datasets by startingIndex |
| scrollOuterStyleOverrides | React.CSSProperties | {} | Custom styles for the outer scroll container |
| scrollInnerStyleOverrides | React.CSSProperties | {} | Custom styles for the inner scroll container |
| className | string | '' | Additional class name for the outer container |
Callbacks
| Callback | Type | Description |
|----------|------|-------------|
| onLoad | (range: { startIndex: number; endIndex: number }) => void | Called when new items become visible and need to be loaded |
| onHide | (range: { startIndex: number; endIndex: number }) => void | Called when items go out of view and are hidden |
| onScroll | (value: number) => void | Called on scroll with current scroll position |
Callback Examples
Using onLoad and onHide for Data Management
import React, { useState, useCallback } from 'react';
import LazyVirtualScroll from '@lazy-virtual-scroll/react';
const DataManagedList = () => {
const [loadedRanges, setLoadedRanges] = useState(new Set());
const [hiddenRanges, setHiddenRanges] = useState(new Set());
const handleLoad = useCallback(({ startIndex, endIndex }) => {
console.log(`Loading items ${startIndex} to ${endIndex}`);
// Track loaded ranges
const rangeKey = `${startIndex}-${endIndex}`;
setLoadedRanges(prev => new Set([...prev, rangeKey]));
// Simulate async data loading
setTimeout(() => {
console.log(`Loaded items ${startIndex} to ${endIndex}`);
}, 100);
}, []);
const handleHide = useCallback(({ startIndex, endIndex }) => {
console.log(`Hiding items ${startIndex} to ${endIndex}`);
// Track hidden ranges for cleanup
const rangeKey = `${startIndex}-${endIndex}`;
setHiddenRanges(prev => new Set([...prev, rangeKey]));
// Optional: Clean up data that's no longer visible
// This can help with memory management for large datasets
}, []);
const handleScroll = useCallback((scrollPosition) => {
console.log(`Scrolled to position: ${scrollPosition}`);
}, []);
return (
<LazyVirtualScroll
totalItems={100000}
itemSize={60}
onLoad={handleLoad}
onHide={handleHide}
onScroll={handleScroll}
render={(index, item) => (
<div style={{
height: '60px',
padding: '10px',
boxSizing: 'border-box' // Ensures padding is included in height
}}>
Item {index} {item ? `- ${item.text}` : '(Loading...)'}
</div>
)}
renderLoading={(index) => (
<div style={{
height: '60px',
padding: '10px',
opacity: 0.6,
boxSizing: 'border-box' // Ensures padding is included in height
}}>
Loading item {index}...
</div>
)}
/>
);
};Working with Fragmented Datasets
For scenarios where your data is loaded in chunks or comes from different sources, you can use the datasets prop instead of data:
const datasets = [
{ startingIndex: 0, data: [{id: 0, text: 'Item 0'}, {id: 1, text: 'Item 1'}, /* ... */] },
{ startingIndex: 100, data: [{id: 100, text: 'Item 100'}, /* ... */] },
// More dataset chunks...
];
<LazyVirtualScroll
datasets={datasets}
totalItems={10000}
itemSize={50}
// ...other props
/>Dynamic Sizing
The component supports dynamic item sizes in two ways:
Manual Size Specification:
const dynamicSizes = { 5: 100, // Item at index 5 has height 100px 10: 200, // Item at index 10 has height 200px }; <LazyVirtualScroll dynamicSizes={dynamicSizes} // ...other props />Automatic Size Detection:
<LazyVirtualScroll autoDetectSizes={true} // ...other props />
Performance Optimization
For optimal performance with large lists:
Use both
scrollThrottleandscrollDebounceto limit scroll event processing:<LazyVirtualScroll scrollThrottle={16} // ~60fps scrollDebounce={100} // Final update after scrolling stops // ...other props />Implement item memoization to prevent unnecessary renders:
const MemoizedItem = React.memo(({ data }) => ( <div>{data.text}</div> )); <LazyVirtualScroll // ... render={(index, item) => <MemoizedItem data={item} />} />
Running unit tests
Run nx test react-lazy-virtual-scroll to execute the unit tests via Vitest.
License
MIT
