DEV Community

Felix Tran
Felix Tran

Posted on

Infinite Scroll Component using useSWRInfinite

In this post, I’ll walk you through creating an infinite scroll component. Whether you’re new to this concept or just need a refresher, we’ll break it down step by step.

This approach was instrumental in developing my product, TubeMemo, where users can efficiently scroll through and manage YouTube video notes without ever feeling overwhelmed by too much information at once.

Tube Memo - Effortless YouTube Note Taking

Capture, annotate, and organize key video moments with seamless transcript integration. Perfect for students, researchers, and content creators

favicon tubememo.com

Overview: What We’re Building

We’re going to create a component that loads a list of blog posts from a server.
We’ll use the useSWRInfinite hook for data fetching and useInView to detect when the user reaches the bottom of the list.

Setting Up the Component StructureOur component will handle:

  • State Management: We’ll track whether we’re in selection mode, which items are selected, and whether the infinite scroll has finished.

  • Fetching Data: We’ll use useSWRInfinite to handle loading paginated data from the server.

  • Rendering Items: The posts will be displayed in a grid, and new posts will load as the user scrolls.

  • Scroll Detection: useInView will help us detect when the user has scrolled to the bottom of the list.

Key Parts of the Code Explained

Let's break down the key parts of the code so you can understand how it all works together.

  • State and Hook Initialization:
  const { ref, inView } = useInView();
  const { userId } = useAuth();
  const [finished, setFinished] = useState(false);
Enter fullscreen mode Exit fullscreen mode

We use useInView to know when a specific element (a placeholder div at the bottom of the list) is visible on the screen. The finished state tracks whether we've loaded all available data.

  • Data Fetching with useSWRInfinite:

  const getKey = (pageIndex: number, previousPageData: any) => {
      if (previousPageData && !previousPageData.length) {
          setFinished(true);
          return null;
      }
      const offset = pageIndex === 0 ? null : previousPageData?.[previousPageData.length - 1]?.id;
      return `/api/posts?offset=${offset}&limit=10`;
  };
Enter fullscreen mode Exit fullscreen mode

The getKey function generates parameters for each API request. If there’s no more data (i.e., previousPageData is empty), it stops further requests by returning null.

The fetcher function is responsible for making the API call:

  interface Post {
      id: string;
      content: string;
  }

  const fetcher = async (url: string): Promise<Post[]> => {
      const response = await fetch(url);
      return response.json();
  };

Enter fullscreen mode Exit fullscreen mode
  • Handling Scroll Events:
  useEffect(() => {
    if (inView && !isLoading && !isValidating && !finished) {
      setSize((prevSize) => prevSize + 1);
    }
  }, [inView, isLoading, isValidating, setSize]);
Enter fullscreen mode Exit fullscreen mode

This useEffect hook checks if the bottom of the list is in view (inView). If it is, and we’re not currently loading or validating data, and we haven’t finished loading all posts, we increment the page size to load more data.

Rendering the Content

The posts are rendered in a grid, and as new data is fetched, it’s appended to the list. We also have a loading state indicator that shows a placeholder while new data is being fetched:

return (
    <div className="relative pt-4 w-full h-full space-y-2">
        <h2 className="text-xl font-semibold">Your posts</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 md:gap-4">
            {items?.map((item: any) => (
                <BlogPost key={item?.id} data={item} />
            ))}
            {isLoading && isValidating ? (
               <div ref={ref}> Loading... </div>
            ) : null}
        </div>
    </div>
);
Enter fullscreen mode Exit fullscreen mode

Putting it all together

import React, { useState, useEffect } from 'react';
import useSWRInfinite from 'swr/infinite';
import { useInView } from '@react-intersection-observer';

const fetcher = async (url: string) => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('Network error');
  }
  return response.json();
};

const InfinityScrollList: React.FC = () => {
  const { ref, inView } = useInView();
  const [finished, setFinished] = useState(false);

  const getKey = (pageIndex: number, previousPageData: any) => {
    if (previousPageData && !previousPageData.length) {
      setFinished(true);
      return null;
    }
    const offset = pageIndex === 0 ? null : previousPageData?.[previousPageData.length - 1]?.id;
    return `/api/posts?offset=${offset}&limit=8`;
  };

  const { data, error, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher);

  useEffect(() => {
    if (inView && !isValidating && !finished) {
      setSize((prevSize) => prevSize + 1);
    }
  }, [inView, isValidating, finished, setSize]);

  const posts = data?.flat();

  return (
    <div className="relative pt-4 w-full h-full space-y-2">
      <h2 className="text-xl font-semibold">Your posts</h2>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 md:gap-4">
        {posts?.map?.((post: any) => (
          <div key={post.id} className="bg-gray-200 p-4 rounded-lg">
            <h3 className="text-lg font-bold">{post.title}</h3>
            <p>{post.content}</p>
          </div>
        ))}
        {(isValidating && size > 1) && (
           <div ref={ref}> Loading... </div>
        )}
      </div>
    </div>
  );
};

export default InfinityScrollList;
Enter fullscreen mode Exit fullscreen mode

Conclusion

Infinite scrolling can greatly enhance user engagement, especially in content-heavy applications. With this guide, you should be well-equipped to implement it in your projects.

Top comments (0)