Infinite scroll
Load more items as the user reaches the bottom of a list.
What it tests
Knowing that there are two ways to do this and that one of them is better. The
repo keeps both: a scroll-handler version, and the IntersectionObserver
version that replaced it.
The approach
A sentinel element sits at the end of the list and an observer watches it. When it intersects the viewport, the next page loads:
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) loadMore();
});The observer is re-attached to the current last element whenever the list
grows, and skipped entirely while loading is true so one scroll can't fire
several loads.
Why not the scroll handler
The commented-out first attempt measures
scrollHeight - (clientHeight + scrollTop) on every scroll event. It works,
but it runs on the main thread dozens of times a second and needs throttling to
be viable. IntersectionObserver is called only when the sentinel crosses the
boundary.
Where it usually goes wrong
- Not disconnecting. Observers accumulate on every re-render without cleanup.
- No
loadingguard. Several rapid intersections fire several fetches. - No end condition. Real data runs out; without a
hasMoreflag it keeps requesting past the last page.
Full solution
1 file from src/folders/infiniteScroll.
index.jsx
// import { useState } from "react";
// function InfiniteScroll() {
// const [data, setData] = useState([...new Array(60)]);
// const [loading, setLoading] = useState(false);
// function handleScroll(e) {
// const { clientHeight, scrollHeight, scrollTop } = e.target;
// const remHeight = scrollHeight - (clientHeight + scrollTop);
// if (remHeight < 20) {
// loadMore();
// }
// }
// function loadMore() {
// setLoading(true);
// setTimeout(() => {
// setData((prev) => [...prev, ...new Array(20)]);
// setLoading(false);
// }, 1000);
// }
// return (
// <div
// onScroll={handleScroll}
// style={{ height: "100vh", overflowY: "auto", border: "1px solid black" }}
// >
// <ul>
// {data.map((_, idx) => (
// <li key={idx}>{idx + 1}</li>
// ))}
// {loading && <p>...loading Data Please Wait</p>}
// </ul>
// </div>
// );
// }
// export default InfiniteScroll;
import { useEffect, useRef, useState } from "react";
function InfiniteScroll() {
const [data, setData] = useState([...new Array(60)]);
const [loading, setLoading] = useState(false);
const observerRef = useRef(null);
function loadMore() {
setLoading(true);
setTimeout(() => {
setData((prev) => [...prev, ...new Array(20)]);
setLoading(false);
}, 1000);
}
const lastElementRef = useRef(null);
useEffect(() => {
if (loading) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
});
if (lastElementRef.current) {
observerRef.current.observe(lastElementRef.current);
}
return () => observerRef.current?.disconnect();
}, [data, loading]);
return (
<div
style={{ height: "100vh", overflowY: "auto", border: "1px solid black" }}
>
<ul>
{data.map((_, idx) => {
const isLast = idx === data.length - 1;
return (
<li key={idx} ref={isLast ? lastElementRef : null}>
{idx + 1}
</li>
);
})}
{loading && <p>...loading Data Please Wait</p>}
</ul>
</div>
);
}
export default InfiniteScroll;