machine coding11
react ui components
IntersectionObserverrefsuseEffect

Infinite scroll

Load more items as the user reaches the bottom of a list.

medium1 min readsolution

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 loading guard. Several rapid intersections fire several fetches.
  • No end condition. Real data runs out; without a hasMore flag 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;
share this post