Created
January 3, 2025 15:16
-
-
Save justgeek/c8df89a1364d1d598d22f9c76d3a8f44 to your computer and use it in GitHub Desktop.
a simple react script to create virtual scroller with performance optimization
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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