Skip to content

Instantly share code, notes, and snippets.

@luizeboli
Created July 11, 2022 13:25
Show Gist options
  • Save luizeboli/f0fb4ff2d2ac91e55e9a3b904af6e8d9 to your computer and use it in GitHub Desktop.
Save luizeboli/f0fb4ff2d2ac91e55e9a3b904af6e8d9 to your computer and use it in GitHub Desktop.
A virtualized list component
import React, { useEffect, useReducer, memo, useRef } from "react";
import debounce from "lodash.debounce";
const VirtualizedList = ({
itemHeight,
getItemsToRender,
overscan,
renderItem,
totalItems,
parentRef,
array
}) => {
const containerRef = useRef();
const [state, dispatch] = useReducer(
(prevState, nextState) => ({ ...prevState, ...nextState }),
{
amountToRender: 0,
innerHeight: totalItems * itemHeight,
topContainerHeight: 0,
bottomContainerHeight: 0,
itemsToRender: [],
previousIndex: 0
}
);
const onScroll = ({ target: { scrollTop } }) => {
const { innerHeight } = state;
const amountToRender = Math.round(
parentRef.current.clientHeight / itemHeight + 2 * overscan
);
const startIndex = Math.max(
0,
Math.floor(scrollTop / itemHeight) - overscan
);
if (
amountToRender === state.amountToRender &&
state.previousIndex === startIndex
)
return;
const itemsToRender = getItemsToRender(startIndex, amountToRender, array);
const topContainerHeight = Math.max(startIndex * itemHeight, 0);
const bottomContainerHeight = Math.max(
innerHeight - topContainerHeight - amountToRender * itemHeight,
0
);
dispatch({
amountToRender,
previousIndex: startIndex,
itemsToRender,
topContainerHeight,
bottomContainerHeight,
windowHeight: window.innerHeight
});
};
useEffect(() => {
onScroll({ target: { scrollTop: containerRef.current.scrollTop } });
}, []);
useEffect(() => {
const debouncedScroll = debounce(
() => onScroll({ target: { scrollTop: containerRef.current.scrollTop } }),
200
);
const resizeListener = () => {
if (state.windowHeight !== window.innerHeight) {
debouncedScroll();
}
};
window.addEventListener("resize", resizeListener);
return () => window.removeEventListener("resize", resizeListener);
}, [state.windowHeight]);
useEffect(() => console.log("Virtual Rendered"));
return (
<div
onScroll={onScroll}
style={{ height: parentRef?.current?.clientHeight, overflowY: "scroll" }}
ref={containerRef}
>
<div style={{ height: state.topContainerHeight }} />
{state.itemsToRender?.map(renderItem)}
<div style={{ height: state.bottomContainerHeight }} />
</div>
);
};
export default memo(VirtualizedList);
// How to use
// Function used by <VirtualizedList />
// It'll receive the array, index and quantity to render
// It should return items to render
const getItemsToRender = (index, amountToRender, data) => {
const items = [];
const startIndex = Math.max(0, index);
const endIndex = Math.min(index + amountToRender, data.length);
for (let i = startIndex; i < endIndex; i += 1) {
items.push(data[i]);
}
return items;
};
<VirtualizedList
array={array}
totalItems={array.length}
itemHeight={25}
overscan={10}
getItemsToRender={getItemsToRender}
renderItem={item => (
<div key={item.Name} style={{ height: 25, padding: "0 2rem" }}>
{item.Name}
</div>
)}
parentRef={parentRef}
/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment