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.
In VIANCH.COM blog InfiniteScroll component will act as a wrapper, providing the infinite scrolling logic to its children.
InfiniteScroll.tsx:InfiniteScrollProps: Defines the props the HOC accepts:
children: The content to be rendered.hasMore: A boolean indicating if there's more data to load.isLoading: A boolean indicating if data is currently being loaded.rootMargin: Margin around the root (viewport) for intersection detection.scrollThreshold: A number between 0 and 1, indicating at what percentage of the target's visibility the observer's callback should be executed.loader: A React element to display while loading.next: A function to call to load the next set of data.useRef hooks:
observerRef: Stores the IntersectionObserver instance.sentinelRef: References a div element at the bottom of the scrollable content. This div acts as our "sentinel" or "trigger" element.firstLoadRef: A flag to prevent immediate loading on initial render if the sentinel is already in view.handleObserver function: This callback is executed when the sentinelRef element intersects with the viewport. It checks hasMore, next, and isLoading to determine if next() should be called.useEffect for IntersectionObserver:
IntersectionObserver with handleObserver as the callback.root: null means the observer will watch for intersection with the viewport.rootMargin and threshold control when the intersection event fires.sentinelRef element.return () => ...) disconnects the observer when the component unmounts.children, the loader (if hasMore or isLoading is true), and the sentinelRef div.Now let's see how a component like GalleryClient uses the InfiniteScroll HOC to implement infinite loading for a gallery of images.
GalleryClient.tsx uses InfiniteScroll:images: Stores the currently displayed images.hasMore: Tracks if there are more images to load.isLoading: Indicates if a fetch operation is in progress.page: Keeps track of the current page number for API calls.calculateHasMore: A helper function to determine if there are more items to fetch based on the current number of images and the total available.fetchNextPage: This asynchronous function is passed to the InfiniteScroll's next prop.
isLoading to true.getCollection to fetch the next page of data.images state by appending the new images.hasMore and page states.isLoading back to false.useEffect for initial hasMore calculation: Ensures hasMore is correctly set on the initial load.InfiniteScroll Usage:
Gallery component (which displays the images) is wrapped inside InfiniteScroll.hasMore, isLoading, and next props are passed from GalleryClient's state and functions.Loading component is provided as the loader.Let's summarize the process of implementing infinite scrolling with this HOC:
InfiniteScroll.tsx ComponentThis involves building the reusable HOC that encapsulates the IntersectionObserver logic.
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.InfiniteScroll.
useRef hooks to manage the IntersectionObserver instance (observerRef), the target element for observation (sentinelRef), and a flag for initial loading (firstLoadRef).handleObserver callback function. This function will be triggered by the IntersectionObserver. Within this function, include the logic to:
entries[0]?.isIntersecting to confirm the sentinel is in view.firstLoadRef to prevent an immediate next call on initial render if the sentinel is already visible.next() function only if the sentinel is intersecting, hasMore is true, a next function is provided, and isLoading is false.useEffect hook to manage the IntersectionObserver's lifecycle.
useEffect, create a new IntersectionObserver instance, passing handleObserver as the callback and configuring root, rootMargin, and threshold based on the component's props.observerRef.current.observe(sentinelRef.current) to start observing your sentinel element.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.{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.InfiniteScroll into Your Content Component (e.g., GalleryClient.tsx)This involves using your newly created InfiniteScroll HOC in a component that displays dynamic data.
InfiniteScroll: Make sure to import the InfiniteScroll component into your content component file.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.fetchNextPage Function: Implement an asynchronous function (e.g., fetchNextPage) that will be responsible for loading the next batch of data. This function should:
isLoading to true at the beginning.page state.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.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).page state (or other pagination parameter).isLoading back to false when the data fetching and state updates are complete.InfiniteScroll: In your component's render method, wrap the component that displays your dynamic data (e.g., your Gallery component) with the InfiniteScroll HOC.
fetchNextPage function as props:
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.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.