Pagination and Infinite Scroll Made Easy with React Query

Pagination and Infinite Scroll Made Easy with React Query

Welcome to this blog on implementing paginated and infinite queries with React Query!

To follow along, make sure you have a basic React project set up. You can create a new project with Vite or any other method you prefer. Additionally, we will be using Axios for API requests and React Query for data fetching (refer to the previous blog post for the basics of React Query).

Implementing Paginated Queries

Pagination is a common requirement in web applications, especially when dealing with large datasets. React Query is a powerful library for managing server state in your React applications, including handling paginated queries with ease.

Paginated Queries allow users to navigate through data using buttons to go to the previous or next pages. The client sends parameters to the server to specify which page of data it wants. Common parameters include the page number and the number of items per page. For example, a request might include page=2 and pageSize=10 to request the second page of data with 10 items per page.

Creating the Component

We will start by creating a simple component PostList.tsx that will fetch and display a list of posts from a mock API 'JSONPlaceholder'.

Here we need a state variable page to keep track of the current page and initialize it to 1. As the page size doesn't change, we don't need a state variable for that, we can declare a local constant pageSize.

We need navigation buttons to go to the previous and next page. Previous page button should be disabled if we are on the first page.

import { useState } from 'react';
import usePosts from './hooks/usePosts';

const PostList = () => {
  const pageSize = 10;
  const [page, setPage] = useState(1);
  const { data: posts, error, isLoading } = usePosts({ page, pageSize });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>{error.message}</p>;

  return (
    <>
      <ul className="list-group">
        {posts?.map((post) => (
          <li key={post.id} className="list-group-item">
            {post.title}
          </li>
        ))}
      </ul>
      <button disabled={page === 1} onClick={() => setPage(page - 1)}>
        Previous
      </button>
      <button onClick={() => setPage(page + 1)}>Next</button>
    </>
  );
};

export default PostList;

Creating the Query Hook

Now let's create a custom hook usePosts.ts where we'll use the useQuery hook from React Query to fetch the posts data.
Here we need to calculate the _start and _limit parameters based on the current page and page size, respectively. _startis the index of our starting position, which we calculate by getting thequery.page. We subtract it by one and multiply the result by page size. The second parameter is_limit which we set to query.pageSize.

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

interface Post {
  id: number;
  title: string;
}

interface PostQuery {
  page: number;
  pageSize: number;
}

const usePosts = (query: PostQuery) =>
  useQuery<Post[], Error>({
    queryKey: ['posts', query],
    queryFn: () =>
      axios
        .get('https://jsonplaceholder.typicode.com/posts', {
          params: {
            _start: (query.page - 1) * query.pageSize,
            _limit: query.pageSize,
          },
        })
        .then((res) => res.data),
    staleTime: 1 * 60 * 1000, //1m
  });

export default usePosts;

Implementing Infinite Queries

Now let us see how to implement infinite queries in a React application using the useInfiniteQuery hook from the @tanstack/react-query package.

Infinite Queries allows us to load data incrementally and handle pagination automatically, making our applications more efficient.

Example 1: Using RAWG API

Creating the Infinite Query Hook

First, let's create a custom hook called useGames to fetch games from an API using Axios. This hook will use useInfiniteQuery to handle pagination automatically.

To implement infinite queries, first, we should use useInfiniteQuery instead of useQuery . Next, we should have our query function(queryFn) receive the page number as a parameter. For implementing Infinite Queries, I'm using RAWG API which supports data pagination using a query parameter called page. So we should pass the page parameter to the backend and set it to pageParam.

Next, we need to implement a function called getNextPageParam which takes two parameters: lastPage and allPages and returns the next page number.

As allPages contains the data for each page which we have retrieved. So, to compute the next page number we'll return allPages.length + 1. Now at some point, we are going to hit the end of the list, so here we check if lastPage.next is truthy we'll return allPages.length + 1, otherwise, we'll return undefined.

import { useInfiniteQuery } from '@tanstack/react-query';
import axios from 'axios';

interface FetchResponse<T> {
  count: number;
  next: string | null;
  results: T[];
}

interface Game {
  id: number;
  name: string;
}

const useGames = () =>
  useInfiniteQuery<FetchResponse<Game>, Error>({
    queryKey: ['games'],
    queryFn: ({ pageParam }) =>
      axios
        .get<FetchResponse<Game>>(
          'https://api.rawg.io/api/games?key=***',
          {
            params: {
              page: pageParam,
            },
          }
        )
        .then((res) => res.data),
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.next ? allPages.length + 1 : undefined;
    },
  });

export default useGames;

Using the Infinite Query Hook in a Component

Next, let's use the useGames hook in a component GameGrid.tsx to display a list of games and a button to load more games.

Now here the data object that we get from the infinite query is not an array of games, it's an instance of infinite data. In this object, we have a couple of properties. One is pages which contains the data for all pages. So we go to data.pages and map each page to a React Fragment. Now inside this fragment, we map each page, which is an array of games, to a bunch of list items.

Now we want to render the button only if there is a next page. So here if hasNextPage property is truthy then we render the button. Now we want to render the label dynamically. So here we can say if we are fetching the next page using property isFetchingNextPage, we return 'Loading more', otherwise we return 'Load More'. Now finally we should handle the click event.

Now when the user clicks on the load more button, the fetchNextPage function is called to get the next page number. At this point, React Query calls getNextPageParam to calculate the next page number and then passes the page number to our query function queryFn.

import React from 'react';
import useGames from './hooks/useGames';

const GameGrid = () => {
  const {
    data,
    error,
    isLoading,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useGames();

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>{error.message}</p>;

  return (
    <>
      <ul>
        {data?.pages.map((page, index) => (
          <React.Fragment key={index}>
            {page.results.map((game) => (
              <li key={game.id}>{game.name}</li>
            ))}
          </React.Fragment>
        ))}
      </ul>
      {hasNextPage && (
        <button onClick={() => fetchNextPage()}>
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
            ? 'Load More'
            : 'Nothing more to load'}
        </button>
      )}
    </>
  );
};

export default GameGrid;

Example 2: Using a mock API (e.g., JSONPlaceholder)

Now let's see how to implement Infinite Querie with the 'JSONPlaceholder' API.

Creating the Infinite Query Hook

import { useInfiniteQuery } from '@tanstack/react-query';
import axios from 'axios';

interface Post {
  id: number;
  title: string;
}

interface PostQuery {
  pageSize: number;
}

const usePosts = (query: PostQuery) => {
  return useInfiniteQuery<Post[], Error>({
    queryKey: ['posts', query],
    queryFn: ({ pageParam }) =>
      axios
        .get('https://jsonplaceholder.typicode.com/posts?', {
          params: {
            _start: ((pageParam as number) - 1) * query.pageSize,
            _limit: query.pageSize,
          },
        })
        .then((res) => res.data),
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.length > 0 ? allPages.length + 1 : undefined;
    },
  });
};
export default usePosts;

Using the Infinite Query Hook in a Component

import React from 'react';
import usePosts from './hooks/usePosts';

const PostList = () => {
  const pageSize = 10;
  const {
    data,
    error,
    isLoading,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = usePosts({ pageSize });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>{error.message}</p>;

  return (
    <>
      <ul>
        {data?.pages.map((page, index) => (
          <React.Fragment key={index}>
            {page.map((post) => (
              <li key={post.id}>
                {post.title}
              </li>
            ))}
          </React.Fragment>
        ))}
      </ul>
      {hasNextPage && (
        <button onClick={() => fetchNextPage()}>
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
            ? 'Load More'
            : 'Nothing more to load'}
        </button>
      )}
    </>
  );
};

export default PostList;

Implementing Infinite Scroll

Now let's make this even better by implementing infinite scroll. So as we scroll down the page, new games get fetched automatically. We'll implement infinite scroll using the popular library called react-infinite-scroll-component. So to install this library as below:

\>npm i react-infinite-scroll-component

Next, let's use 'Example 1' from above where we created the GameGrid component and useGames hook. The hook will remain the same, we'll make some modifications to the GameGrid component.

So go to the GameGrid component and wrap the list of games inside an <InfiniteScroll> component. Now this component has several props.

  • dataLength: The first prop is dataLength , to specify the total number of items we have fetched so far, which we have to compute.

    For this purpose, we used a constant called fetchedGamesCount. So we have data.pages and we can use the reduce method to reduce this array of pages into a number that represents the total number of games we have fetched so far. So we know that each page contains an array of games. Here we are using the reduce method to combine the number of games on each page into a number. Initially, the data is undefined, so we should give this a default value of 0. Now we can use this to set the data length prop.

  • hasMore: Next, we have the hasMore prop to indicate whether there are more items to fetch, we can set this to hasNextPage which we got from our infinite query. But the type of this property is boolean or undefined. But this component expects a boolean. That is why we can apply double exclamations here to convert this to an actual boolean value. So if we have undefined here by applying double exclamations, undefined will be converted to boolean false.

  • next: Next, we set the next prop. Here we pass a function and in this function, we call fetchNextPage(). This is exactly like how we handled the click event of the load more button.

  • loader and endMessage: Finally, we set loader and endMessage props to respective text messages.

import React from 'react';
import useGames from './hooks/useGames';
import InfiniteScroll from 'react-infinite-scroll-component';

const GameGrid = () => {
  const { data, error, isLoading, fetchNextPage, hasNextPage } = useGames();

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>{error.message}</p>;

  const fetchedGamesCount =
    data?.pages.reduce((total, page) => total + page.results.length, 0) || 0;

  return (
    <InfiniteScroll
      dataLength={fetchedGamesCount}
      //  dataLength is the total no of items fetched so far
      hasMore={!!hasNextPage}
      // by applying !!, undefined value turns into false
      next={() => fetchNextPage()}
      loader={<p>Loading...</p>}
      endMessage={<p>You have seen it all!</p>}
    >
      {/* Render your game items here */}
    </InfiniteScroll>
  );
};

export default GameGrid;

As the user scrolls down, more games will be fetched automatically using the fetchNextPage function provided by React Query.

Conclusion

So we've learned how to implement paginated queries using React Query, which allows users to navigate through data using buttons to go to the previous and next pages.

We also learned how to implement infinite queries in a React application. This approach allows us to load data incrementally and handle pagination automatically, making our applications more efficient and responsive. Then we learned about Infinite Scroll which is a great way to improve user experience by dynamically loading content as the user scrolls.

I hope you found this helpful. Happy coding!