Building an Infinite Scroll Higher-Order Component

Building an Infinite Scroll Higher-Order Component

June 10, 2025

Infinite scrolling is a popular UI pattern that loads content continuously as the user scrolls down a page, eliminating the need for pagination, for that reason every gallery and page on vianch.com is using it. In this blog post, we'll explore how to create a reusable Infinite Scroll Higher-Order Component (HOC) in React, making it easy to add this functionality to any component.

We'll use an IntersectionObserver to detect when the user reaches the bottom of the scrollable area and then trigger a function to load more data.

1. Understanding the InfiniteScroll HOC

In VIANCH.COM blog InfiniteScroll component will act as a wrapper, providing the infinite scrolling logic to its children.

Key aspects of InfiniteScroll.tsx:

2. Consuming the InfiniteScroll HOC.

Now let's see how a component like GalleryClient uses the InfiniteScroll HOC to implement infinite loading for a gallery of images.

How GalleryClient.tsx uses InfiniteScroll:

3. Step-by-Step Implementation

Let's summarize the process of implementing infinite scrolling with this HOC:

Step 1: Create the InfiniteScroll.tsx Component

This involves building the reusable HOC that encapsulates the IntersectionObserver logic.

  1. Define InfiniteScrollProps: Start by creating a TypeScript interface to clearly define all the props that your InfiniteScroll component will accept. This includes children, hasMore, isLoading, rootMargin, scrollThreshold, loader, and next.
  2. Implement the Functional Component: Create a functional React component called InfiniteScroll.
    • Utilize useRef hooks to manage the IntersectionObserver instance (observerRef), the target element for observation (sentinelRef), and a flag for initial loading (firstLoadRef).
    • Implement the handleObserver callback function. This function will be triggered by the IntersectionObserver. Within this function, include the logic to:
      • Check entries[0]?.isIntersecting to confirm the sentinel is in view.
      • Manage the firstLoadRef to prevent an immediate next call on initial render if the sentinel is already visible.
      • Conditionally call the next() function only if the sentinel is intersecting, hasMore is true, a next function is provided, and isLoading is false.
    • Use a useEffect hook to manage the IntersectionObserver's lifecycle.
      • Inside the useEffect, create a new IntersectionObserver instance, passing handleObserver as the callback and configuring root, rootMargin, and threshold based on the component's props.
      • Call observerRef.current.observe(sentinelRef.current) to start observing your sentinel element.
      • Provide a cleanup function within the useEffect's return statement (return () => observerRef.current.disconnect();). This is crucial for preventing memory leaks by disconnecting the observer when the component unmounts or before the useEffect re-runs.
    • In the component's render method, return:
      • {children}: The content that InfiniteScroll is wrapping.
      • {(hasMore || isLoading) && loader}: Your loading indicator, shown when more data is available or being fetched.
      • <div ref={sentinelRef} />: The invisible div at the bottom that acts as the trigger for loading more content.

Step 2: Integrate InfiniteScroll into Your Content Component (e.g., GalleryClient.tsx)

This involves using your newly created InfiniteScroll HOC in a component that displays dynamic data.

  1. Import InfiniteScroll: Make sure to import the InfiniteScroll component into your content component file.
  2. Manage Component State: Within your content component, set up the necessary state variables using useState hooks:
    • [items, setItems]: To hold the array of data that will be displayed (e.g., images in GalleryClient).
    • [hasMore, setHasMore]: A boolean to track if there are more items to load from your data source.
    • [isLoading, setIsLoading]: A boolean to indicate whether a data fetching operation is currently active.
    • [page, setPage] (or similar): To manage pagination parameters for your API calls.
  3. Create the fetchNextPage Function: Implement an asynchronous function (e.g., fetchNextPage) that will be responsible for loading the next batch of data. This function should:
    • Set isLoading to true at the beginning.
    • Make an API call (or fetch data from your data source) to retrieve the next set of items, typically by incrementing your page state.
    • Update your items state by appending the newly fetched data to the existing data. Do not replace the existing data, as this would break the infinite scroll.
    • Update the hasMore state based on whether all available data has now been loaded (e.g., by comparing the total number of items loaded so far against the total available items from your backend).
    • Increment your page state (or other pagination parameter).
    • Set isLoading back to false when the data fetching and state updates are complete.
  4. Wrap Your Content with InfiniteScroll: In your component's render method, wrap the component that displays your dynamic data (e.g., your Gallery component) with the InfiniteScroll HOC.
    • Pass the state variables and the fetchNextPage function as props:
  5. Initialize hasMore: Ensure that your initial data load correctly sets the hasMore state. You can do this in a useEffect hook that runs once on component mount or when initial props change.

Conclusion

By carefully creating a dedicated InfiniteScroll HOC, you've successfully encapsulated the complex IntersectionObserver logic into a self-contained, reusable component. This powerful pattern promotes cleaner code by separating concerns, enhances maintainability, and makes it incredibly straightforward to add seamless infinite scrolling capabilities to various parts of your React application. You now have a robust solution for dynamically loading content as users scroll, significantly improving the user experience on your site.