DEV Community

Cover image for How to use Infinity Queries (TanStack Query) to do infinite scrolling
Davi Rezende
Davi Rezende

Posted on

How to use Infinity Queries (TanStack Query) to do infinite scrolling

Here's your post translated into English:


In this post, I’ll teach you how to implement infinite scrolling using TanStack Query, specifically with Infinity Queries. We’ll create a photo feed with Vite and set up infinite scrolling. To start, open your terminal and run the following command to clone a project with basic configurations:

git clone --branch start https://github.com/DAVI-REZENDE/photos-feed.git

cd photos-feed
npm i
Enter fullscreen mode Exit fullscreen mode

All set! Now, let's implement the infinite scroll functionality using the TanStack Query library. Install it with the command below:

npm i @tanstack/react-query
npm i axios
Enter fullscreen mode Exit fullscreen mode

In the App.tsx file, you'll see that your code looks like this:

Image description

First, we’ll replace useEffect with useInfiniteQuery, the hook responsible for managing our infinite requests. We must provide it with two properties: queryKey and queryFn, as follows:

const { 
  data, 
  isLoading,
  fetchNextPage,
  isFetchingNextPage, 
  isFetching, 
  hasNextPage 
} = useInfiniteQuery({
    queryFn: fetchPhotos,
    queryKey: ['photos'],
    initialPageParam: 1,
    getNextPageParam: (lastPage) => {
      return lastPage.nextPage
    }
})
Enter fullscreen mode Exit fullscreen mode

Explanation of each parameter:

  • queryFn: The function responsible for returning our request data; it receives the current page as a parameter.
  • queryKey: Serves as an identifier for your request, also acting as a dependency array. Every time a variable you pass within it changes, useInfiniteQuery automatically refetches.
  • initialPageParam: The initial default value.
  • getNextPageParam: Receives everything your queryFn function returns and must return the next page number to be requested.

We’ll need to modify the fetchPhotos function:

async function fetchPhotos({ pageParam }: { pageParam: number }) {
  const response = await api.get<ImageData[]>('/photos', {
    params: {
      page: pageParam,
      per_page: 5,
    }
  })

  return {
    data: response.data,
    nextPage: pageParam + 1
  }
} 
Enter fullscreen mode Exit fullscreen mode

The useInfiniteQuery hook returns the data in pages, so our rendering will change slightly:

<main className="h-screen w-screen bg-zinc-950 flex flex-col gap-6 p-6 items-center text-white overflow-auto">
  {isLoading ? 'Loading...' : (
    <>
      {data?.pages.map((group, i) => (
        <div className="flex flex-col gap-6" key={i}>
          {group.data.map(({ id, urls }) => (
            <img className="aspect-square rounded-md h-[550px] object-cover" src={urls.regular} key={id} />
          ))}
        </div>
      ))}
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
              ? 'Load More'
              : 'Nothing more to load'}
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </>
  )}
</main>
Enter fullscreen mode Exit fullscreen mode

Now, each time the user reaches the end of the scroll and clicks the ‘Load More’ button, the data will be automatically appended.

To fetch the next page whenever the user reaches the end of the scroll without needing to click the button, just add the following function:

function handleScroll(event: UIEvent<HTMLElement>) {
  const { scrollTop, clientHeight, scrollHeight } = event.currentTarget

  if (scrollTop + clientHeight >= scrollHeight) {
    fetchNextPage()
  }
}
Enter fullscreen mode Exit fullscreen mode

And add the onScroll event in the div that wraps your list, calling the function there. Done! Now, every time the user scrolls to the end, new data will automatically load.

In the end, your code should look like this:

import { useInfiniteQuery } from "@tanstack/react-query"
import { UIEvent } from "react"
import { api } from "./lib/api"

type ImageData = {
  id: string,
  urls: {
    regular: string
  }
}

export function Home() {
  async function fetchPhotos({ pageParam }: { pageParam: number }) {
    const response = await api.get<ImageData[]>('/photos', {
      params: {
        page: pageParam,
        per_page: 5,
      }
    })

    return {
      data: response.data,
      nextPage: pageParam + 1
    }
  } 

  const { data, isLoading, fetchNextPage, isFetchingNextPage, isFetching, hasNextPage } = useInfiniteQuery({
    queryFn: fetchPhotos,
    queryKey: ['photos'],
    initialPageParam: 1,
    getNextPageParam: (lastPage) => {
      return lastPage.nextPage
    }
  })

  function handleScroll(event: UIEvent<HTMLElement>) {
    const { scrollTop, clientHeight, scrollHeight } = event.currentTarget

    if (scrollTop + clientHeight >= scrollHeight) {
      fetchNextPage()
    }
  };


  return (
    <main className="h-screen w-screen bg-zinc-950 flex flex-col gap-6 p-6 items-center text-white overflow-auto" onScroll={handleScroll}>
      {isLoading ? 'Loading...' : (
        <>
          {data?.pages.map((group, i) => (
            <div className="flex flex-col gap-6" key={i}>
              {group.data.map(({ id, urls }) => (
                <img className="aspect-square rounded-md h-[550px] object-cover" src={urls.regular} key={id} />
              ))}
            </div>
          ))}
          <div>
            <button
              onClick={() => fetchNextPage()}
              disabled={!hasNextPage || isFetchingNextPage}
            >
              {isFetchingNextPage
                ? 'Loading more...'
                : hasNextPage
                  ? 'Load More'
                  : 'Nothing more to load'}
          </button>
          </div>
          <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
        </>
      )}
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

Thank you!

Top comments (0)