Infinite Scroll React Example with TypeScript and NextJS

  • August 22, 2022
  • Royce Threadgill
  • 8 min read

For the uninitiated, infinite scroll (otherwise known as endless scroll) refers to a method of automatically loading data when a user scrolls to the bottom of their screen, allowing them to continue browsing content with minimal effort. If you’ve been in the front-end game since the good bad old days when jQuery was bleeding edge, that probably sounds like an irritating feature to build. However, with modern browser APIs and JavaScript libraries like React, it is orders of magnitude less painful to create this effect.

In this article, we’ll look at a few libraries that can facilitate infinite scroll in React. In case that’s not your style, we’ll also dive into an infinite scroll example that leverages the Intersection Observer API and NextJS, touching on infinite scroll SSR (server-side rendering) tactics that can help with search engine optimization.

Why Implement Infinite Scroll?

When loading a page, it’s generally preferable to load as little data as possible. This helps the page load faster, which improves user experience and search rankings.

If you have a large amount of data, you’ll need to split that data up to keep your page loading quickly. That’s where pagination strategies like infinite scroll come in. Pagination refers to the practice of fetching your data in small chunks (or pages), rather than retrieving all of your data at once.

Many websites implement pagination via page buttons, but infinite scroll is thought to reduce friction since it requires less user interaction. This makes infinite scroll a popular request from clients aiming to improve conversion on their websites. Infinite scroll is especially sought after in e-commerce applications, where getting eyeballs on products is of paramount importance for conversion rates.

Libraries to Facilitate React Infinite Scroll

If you prefer not to roll your own custom infinite scroll solution, there are many libraries that can simplify building the feature. Here is a non-exhaustive list of options:

  • react-waypoint: As far as ready-made solutions go, this is a personal favorite of mine. The library is simple to use and you can implement infinite scroll with little more than a Waypoint component and an onEnter handler. With 4,000 stars on GitHub as of August 2022, react-waypoint is far and away the most popular of the libraries listed here, though it’s also a more general-use library than its counterparts.
  • react-infinite-scroll-hook: This is another library that stood out from the crowd. I haven’t personally used it, but it seems to be more actively maintained than react-infinite-scroll-component. On top of that, it happens to weigh in at a paltry 1.8 kB minified bundle size, according to bundlephobia, which appeals to the minimalist in me.
  • react-infinite-scroll-component: I found this seemingly popular component in the course of my research for this article. I haven’t used it myself and can’t vouch for it, but it kept popping up so I felt I should add it to this list. It’s worth noting that as of August 2022, this library has 114 open GitHub issues; compared against react-infinite-scroll-hook (2 open issues) and react-waypoint (54 open issues), that’s a relatively high number of problems and could be cause for concern.

Custom Infinite Scroll with TypeScript and NextJS

For those that require (or desire) a custom solution, have no fear: it is surprisingly straightforward to DIY your own infinite scroll feature.

To help you get started, we’ll be guiding you through an example app that uses React, TypeScript, and NextJS. If you’re unfamiliar with NextJS, you can think of it as a specialized framework built on top of React itself. Don’t worry – a custom infinite scroll React solution is almost identical to a custom infinite scroll NextJS solution. The main reason I chose NextJS for this example was simply to highlight the interaction between server side rendering, infinite scroll, and search engine optimization.

Setting the Stage for our Infinite Scroll Example

Our sample app is a blog. When a user navigates to the home page of our app, they’re shown a set of blog posts; you can think of this as “page 1” of the blog. When a user reaches the bottom of the home page, more blog posts are loaded via an API call. It looks something like this:

infinite scroll sample app

 

Load the Initial Data

In this example, we’re going to use the getServerSideProps method from NextJS to get our initial data, then add that initial posts data to our Home component. After page 1 is loaded, we’ll make additional API calls and add the retrieved data to a dynamicPosts array.

// index.tsx (your home page)
export interface BlogPost {
 id: string;
 title: string;
 description: string;
}
 
export interface HomeProps {
 posts: BlogPost[];
}
 
 
export async function getServerSideProps() {
 const posts = [
   {
     id: uuidv4(), // creates a unique ID for the post
     title: "Blog post 1",
     description:
       "Lorem ipsum dolor sit amet consectetur adipisicing elit. Possimus quae numquam repudiandae ab asperiores exercitationem nulla, enim debitis necessitatibus quaerat incidunt nesciunt. Soluta sapiente quisquam magni, quas odit tempora ullam!",
   },
   // more data here
 ];
 
 return {
   props: {
      posts,
      total: 20
   },
 };
}

const Home: NextPage<HomeProps> = ({ posts }) => {
 const {
   isLoading,
   loadMoreCallback,
   hasDynamicPosts,
   dynamicPosts,
   isLastPage,
 } = useInfiniteScroll(posts);

 return (
   <HomePage
     posts={hasDynamicPosts ? dynamicPosts : posts}
     isLoading={isLoading}
     loadMoreCallback={loadMoreCallback}
     isLastPage={isLastPage}
   />
 );
};
 
export default Home;

This essentially means that page 1 of our blog is server-side rendered and should be accessible to search engine crawlers, while all additional data is retrieved client-side. You can use this hybrid infinite scroll SSR strategy if you want search engines to read the first set of data loaded on your infinite scroll website. 

If none of that matters to you, you can think of this as a simple API call. Just remember that the data retrieved via that first client-side API call likely won’t be accessible to search engines.

Create Your Custom Hook

You probably noticed in the above example that there’s a custom hook called useInfiniteScroll. The vast majority of the logic governing infinite scroll behavior in this example can be found within that hook.

// useInfiniteScroll.ts
export interface UseInfiniteScroll {
 isLoading: boolean;
 loadMoreCallback: (el: HTMLDivElement) => void;
 hasDynamicPosts: boolean;
 dynamicPosts: BlogPost[];
 isLastPage: boolean;
}
 
export const useInfiniteScroll = (posts: BlogPost[]): UseInfiniteScroll => {
 const [isLoading, setIsLoading] = useState(false);
 const [page, setPage] = useState(1);
 const [hasDynamicPosts, setHasDynamicPosts] = useState(false);
 const [dynamicPosts, setDynamicPosts] = useState<BlogPost[]>(posts);
 const [isLastPage, setIsLastPage] = useState(false);
 const observerRef = useRef<IntersectionObserver>();
 const loadMoreTimeout: NodeJS.Timeout = setTimeout(() => null, 500);
 const loadMoreTimeoutRef = useRef<NodeJS.Timeout>(loadMoreTimeout);
 
 const handleObserver = useCallback(
   (entries: any[]) => {
     const target = entries[0];
     if (target.isIntersecting) {
       setIsLoading(true);
       clearTimeout(loadMoreTimeoutRef.current);
 
       // this timeout debounces the intersection events
       loadMoreTimeoutRef.current = setTimeout(() => {
         axios.get(`/api/posts/${page}`).then((resp) => {
           setPage(page + 1);
           const newPosts = resp?.data["posts"];
 
           if (newPosts?.length) {
             const newDynamicPosts = [...dynamicPosts, ...newPosts];
             setDynamicPosts(newDynamicPosts);
             setIsLastPage(newDynamicPosts?.length === resp?.data["total"]);
             setHasDynamicPosts(true);
             setIsLoading(false);
           }
         });
       }, 500);
     }
   },
   [loadMoreTimeoutRef, setIsLoading, page, dynamicPosts]
 );
 
 const loadMoreCallback = useCallback(
   (el: HTMLDivElement) => {
     if (isLoading) return;
     if (observerRef.current) observerRef.current.disconnect();
 
     const option: IntersectionObserverInit = {
       root: null,
       rootMargin: "0px",
       threshold: 1.0,
     };
     observerRef.current = new IntersectionObserver(handleObserver, option);
 
     if (el) observerRef.current.observe(el);
   },
   [handleObserver, isLoading]
 );
 
 return {
   isLoading,
   loadMoreCallback,
   hasDynamicPosts,
   dynamicPosts,
   isLastPage,
 };
};

The loadMoreCallback method is what we’ll use to determine if a user has “intersected” a given element. As you may have guessed, the magic browser API helping us do that is called “Intersection Observer”. We pass a handler method handleObserver and initialization options to IntersectionObserver, which will handle emitted events. If the user is intersecting our target element, then target.isIntersecting is true and an API call is made to retrieve more data.

Please note that the aforementioned API call is wrapped in a timeout. Intersection Observer may fire multiple intersecting events when the user reaches the target, so we want to debounce those events to avoid making unnecessary calls to our API. We do that by only executing the API call if no other intersecting events have occurred within the last 500 milliseconds.

Add Your Intersecting Component

Now you’re probably wondering how we define where the intersection target is. We do that by adding the loadMoreCallback method as a ref to our desired component. Importantly, we’ll also re-render the intersecting component after new data is loaded to ensure that our intersection target moves along with the bottom of the page.

// Loader.tsx
type LoaderProps = Pick<
 UseInfiniteScroll,
 "isLoading" | "loadMoreCallback" | "isLastPage"
>;
 
export const Loader = ({
 isLoading,
 isLastPage,
 loadMoreCallback,
}: LoaderProps) => {
 if (isLoading) return <p>Loading...</p>;
 
 if (isLastPage) return <p>End of content</p>;
 
 return <div ref={loadMoreCallback}>load more callback</div>;
};

We then add that component below our already-loaded content, so that the Intersection Observer will fire an intersection event at the bottom of the page.

// HomePage.tsx
type HomePageProps = Pick<
 UseInfiniteScroll,
 "isLoading" | "loadMoreCallback" | "isLastPage"
> & {
 posts: BlogPost[];
};
 
export const HomePage = ({
 posts,
 isLoading,
 loadMoreCallback,
 isLastPage,
}: HomePageProps) => {
 return (
   <main className={styles.container}>
     <h1 className={styles.heading}>Infinite Scroll Demo</h1>
     <p>It is a very nice app</p>
     {posts.map((post) => (
       <div className={styles.blogPost} key={post.id}>
         <img src="https://picsum.photos/500" alt="random image" />
         <div>
           <h2>{post.title}</h2>
           <p>{post.description}</p>
         </div>
       </div>
     ))}
 
     >Loader
       isLoading={isLoading}
       isLastPage={isLastPage}
       loadMoreCallback={loadMoreCallback}
     />
   </main>
 );
};

Wrapping Up Our React Infinite Scroll Example

That’s it! With a couple of components and a custom hook, you can create your own infinite scrolling effect with React, TypeScript, and NextJS.

Given its ubiquity in consumer software (e.g., social media sites), infinite scrolling has become the go-to pagination strategy for many companies. So if you’ve been lucky enough to dodge infinite scroll so far, I wouldn’t bet on that trend continuing. When you’re inevitably asked to add this feature to an app, I hope this guide can be of help.