How to deal with infinite scroll in React

Infinite scrolling has become a popular feature in modern web applications, allowing users to seamlessly load and view content as they scroll down the page. Implementing infinite scrolling in React can be a bit challenging, but with the help of the react-query library, it becomes much easier. In this article, we'll explore how to handle infinite scrolling in React using the useInfiniteQuery hook from react-query.
Let's dive into the code and understand how it works.

import { useInfiniteQuery } from 'react-query';
import { useState } from 'react';

type Post = {
  id: number;
  userId: number;
  title: string;
  body: string;
};

const fetchPosts = async (
  page: number,
  limit: number,
  signal: AbortSignal | undefined
): Promise<{
  totalCount: number;
  posts: Post[];
}> => {
  const response = await fetch(`api/v1/posts?_page=${page + 1}&_limit=${limit}`, { signal });

  if (!response.ok) {
    throw new Error(`Failed to fetch: ${response.status}`);
  }

  const posts: Post[] = await response.json();
  const totalCount = Number.parseInt(response.headers.get('x-total-count') || '0', 10); // fetch total from server headers

  return {
    totalCount,
    posts
  };
};

interface UsePosts {
  posts: Post[];
  isLoading: boolean;
  isFetching: boolean;
  error?: string;
  hasNext: boolean;
  next?: () => void;
}

export const usePosts = (): UsePosts => {
  const [limit] = useState<number>(10);

  const {
    data,
    fetchNextPage,
    isLoading,
    isFetching,
    error
  } = useInfiniteQuery(['posts'], fetchPosts, {
    refetchOnWindowFocus: false,
    retry: 3,
    getNextPageParam: (lastPage, pages) => {
      if (Math.ceil(lastPage.totalCount / limit) > pages.length) {
        return pages.length;
      }
      return undefined;
    }
  });

  return {
    posts: data?.pages.flatMap(({ posts }) => posts) || [],
    isLoading,
    isFetching,
    error: error?.message,
    next: fetchNextPage,
    hasNext: data?.pageParams.length > 0 || false
  };
};

The code begins by importing the necessary dependencies: React, useInfiniteQuery from react-query, and useState from react. These dependencies are essential for building a React component and managing state.
The post type represents the structure of a post object. It defines the properties of a post, such as id, userId, title, and body. This type is used to provide type safety when dealing with post data.
Next, we have the fetchPosts function, which is an asynchronous function responsible for fetching posts from the server. It takes three parameters: page, limit, and signal. The page parameter represents the current page number, limit indicates the number of posts to fetch per page, and signal is an optional abort signal used to cancel the request if necessary.
Inside the fetchPosts function, an HTTP request is made using the fetch API to retrieve the posts from the server. The URL is constructed based on the page and limit parameters. If the response is not successful (i.e., the response.ok property is false), an error is thrown with the appropriate status code.
Assuming the response is successful, the posts are extracted from the response body using response.json(). Additionally, the x-total-count header from the server response is parsed to get the total number of posts. The totalCount and posts are then returned as an object.
Next, we have the UsePosts interface, which defines the form of the return value from the usePosts hook. It includes properties such as posts (an array of posts), isLoading (a boolean indicating whether the data is currently being loaded), isFetching (a boolean indicating whether the data is currently being fetched), error (an optional string representing an error that occurred during the data fetching process), hasNext (a boolean indicating whether there are more posts to fetch), and next (a function to fetch the next page of posts).
Now let's explore the usePosts hook, which encapsulates the logic for handling infinite scrolling. It starts by declaring a state variable limit using the useState hook, which represents the number of posts to fetch per page. The initial value is set to 10.
The useInfiniteQuery hook is then used to handle the fetching and scrolling. It takes three arguments: the query key, the fetch function fetchPosts, and an options object.
The query key posts is an array that identifies the query. It helps react-query cache and manage the data associated with the query.
The fetchPosts function is passed as the fetch function. It is called internally by useInfiniteQuery to fetch the data for each page.
The Options object provides additional configuration for the useInfiniteQuery hook. In this case, we set refetchOnWindowFocus to false to prevent automatic refetching of data when the window regains focus. The retry option is set to 3, indicating that the fetch function should be retried up to three times if it fails. Finally, the getNextPageParam option is a function that determines the parameter for fetching the next page. It checks if the number of pages fetched so far is less than the total number of pages available. If there are more pages, it returns the length of the pages array, indicating the next page to fetch. Otherwise, it returns undefined, indicating that there are no more pages to fetch.
Finally, the usePosts hook returns an object with the necessary data to render the posts and manage the state. The posts property is obtained by flattening the data.pages array and extracting the posts from each page. If the data is not available, an empty array is returned. Other properties include isLoading (indicating whether the data is currently being loaded), isFetching (indicating whether the data is currently being fetched in the background), error (an optional string representing an error that occurred during the data fetching process), next (a function to fetch the next page of posts), and hasNext (a boolean indicating whether there are more posts to fetch).

By using the usePosts hook, you can easily implement infinite scrolling functionality in your React application, making it more usable and efficient when dealing with large datasets.