Skip to content

Instantly share code, notes, and snippets.

@justgeek
Created January 3, 2025 15:16
Show Gist options
  • Save justgeek/c8df89a1364d1d598d22f9c76d3a8f44 to your computer and use it in GitHub Desktop.
Save justgeek/c8df89a1364d1d598d22f9c76d3a8f44 to your computer and use it in GitHub Desktop.
a simple react script to create virtual scroller with performance optimization
/* eslint-disable react/prop-types */
// inspired from this great article https://blog.logrocket.com/virtual-scrolling-core-principles-and-basic-implementation-in-react/
import React, { useRef, useEffect, useState } from 'react';
import { shape, func, string, number } from 'prop-types';
import './VirtualScroller.scss';
export function VirtualScroller(props) {
const { settings, row, generateItems } = props;
const viewportElement = useRef();
const [state, setState] = useState({ data: [] });
const {
itemHeight,
amount,
tolerance,
minIndex,
maxIndex,
initialScrollItemClass,
viewportFrameHeight,
initialScrollItemIndex,
} = settings;
const viewportHeight = viewportFrameHeight || amount * itemHeight;
const totalHeight = (maxIndex - minIndex + 1) * itemHeight;
const toleranceHeight = tolerance * itemHeight;
const bufferedItems = amount + 2 * tolerance;
useEffect(() => {
const inititalScrollIndex = initialScrollItemIndex || 0;
const scrollTop = parseInt(inititalScrollIndex * itemHeight);
handleScroll({ target: { scrollTop } });
setTimeout(() => {
viewportElement.current.scrollTop = scrollTop; // first make sure item exists in dom
// this for adjusting scroll to exactly match item offset top
const currentItemOffset = viewportElement.current.querySelector(`.${initialScrollItemClass}`)?.offsetTop;
if (currentItemOffset) {
viewportElement.current.scrollTop = currentItemOffset - viewportElement.current.offsetTop;
}
}, 0);
}, []);
const handleScroll = ({ target: { scrollTop } }) => {
const index = minIndex + Math.floor((scrollTop - toleranceHeight) / itemHeight);
const data = generateItems(index, bufferedItems);
const topPaddingHeight = Math.max((index - minIndex) * itemHeight, 0);
const bottomPaddingHeight = Math.max(totalHeight - topPaddingHeight - data.length * itemHeight, 0);
setState({
topPaddingHeight,
bottomPaddingHeight,
data,
});
};
return (
<virtual-scroller>
<div className="viewport" ref={viewportElement} onScroll={handleScroll} style={{ height: viewportHeight }}>
<div style={{ height: state.topPaddingHeight }}></div>
{state.data.map(row)}
<div style={{ height: state.bottomPaddingHeight }}></div>
</div>
</virtual-scroller>
);
}
VirtualScroller.propTypes = {
/**
A function to generate data with offset, and limit
returns: list of data to be rendered
*/
generateItems: func,
/**
Render function of each row
*/
row: func,
/**
Virtual scroller settings
*/
settings: shape({
viewportFrameHeight: number,
itemHeight: number,
amount: number,
tolerance: number,
minIndex: number,
maxIndex: number,
initialScrollItemIndex: number,
initialScrollItemClass: string,
}),
};
VirtualScroller.defaultProps = {};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment