Skip to main content

Infinite Queries

Model-written

contextclaimmethodsetupresultanalysislimitation

Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. TanStack Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists.Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. TanStack Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists.useInfiniteQuery queries load more and infinite scroll lists It extends useQuery for lists that additively load more data onto an existing setuseInfiniteQuery queries load more and infinite scroll listsuseInfiniteQuery

When using useInfiniteQuery, you'll notice a few things are different:

  • data is now an object containing infinite query data:
  • data.pages array containing the fetched pages
  • data.pageParams array containing the page params used to fetch the pages
  • The fetchNextPage and fetchPreviousPage functions are now available (fetchNextPage is required)
    The fetchNextPage and fetchPreviousPage functions are now available (fetchNextPage is required)
    fetchNextPage and fetchPreviousPage functions are available fetchNextPage is required while fetchPreviousPage is optional
    fetchNextPage and fetchPreviousPage functions are available
    fetchNextPage
  • The initialPageParam option is now available (and required) to specify the initial page param
    The initialPageParam option is now available (and required) to specify the initial page param
    initialPageParam option is required to set the initial page param It supplies the starting page param for the first request
    initialPageParam option is required to set the initial page param
    initialPageParam
  • The getNextPageParam and getPreviousPageParam options are available for both determining if there is more data to load and the information to fetch it. This information is supplied as an additional parameter in the query function
    The getNextPageParam and getPreviousPageParam options are available for both determining if there is more data to load and the information to fetch it. This information is supplied as an additional parameter in the query function
    getNextPageParam and getPreviousPageParam determine more data and its info That information is supplied as an additional parameter in the query function
    getNextPageParam and getPreviousPageParam determine more data and its info
    getNextPageParam
  • A hasNextPage boolean is now available and is true if getNextPageParam returns a value other than null or undefined
    A hasNextPage boolean is now available and is true if getNextPageParam returns a value other than null or undefined
    hasNextPage is true when getNextPageParam returns a value It is false if getNextPageParam returns null or undefined
    hasNextPage is true when getNextPageParam returns a value
    hasNextPage
  • A hasPreviousPage boolean is now available and is true if getPreviousPageParam returns a value other than null or undefined
    A hasPreviousPage boolean is now available and is true if getPreviousPageParam returns a value other than null or undefined
    hasPreviousPage is true when getPreviousPageParam returns a value It is false if getPreviousPageParam returns null or undefined
    hasPreviousPage is true when getPreviousPageParam returns a value
    hasPreviousPage
  • The isFetchingNextPage and isFetchingPreviousPage booleans are now available to distinguish between a background refresh state and a loading more state
    The isFetchingNextPage and isFetchingPreviousPage booleans are now available to distinguish between a background refresh state and a loading more state
    isFetchingNextPage and isFetchingPreviousPage distinguish loading more They separate background refresh state from loading more state
    isFetchingNextPage and isFetchingPreviousPage distinguish loading more
    isFetchingNextPage

Note: Options initialData or placeholderData need to conform to the same structure of an object with data.pages and data.pageParams properties.

Note: Options initialData or placeholderData need to conform to the same structure of an object with data.pages and data.pageParams properties.
initialData and placeholderData must match pages and pageParams structure They need the same object shape with data.pages and data.pageParams properties
initialData and placeholderData must match pages and pageParams structure
initialData

Example

Build a Load More UI with getNextPageParam and fetchNextPage

Let's assume we have an API that returns pages of projects 3 at a time based on a cursor index along with a cursor that can be used to fetch the next group of projects:Let's assume we have an API that returns pages of projects 3 at a time based on a cursor index along with a cursor that can be used to fetch the next group of projects:API returns projects 3 at a time with a cursor Each fetch passes a cursor index and receives a nextCursor for the following groupAPI returns projects 3 at a time with a cursorcursor

fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }

With this information, we can create a "Load More" UI by:

  • Waiting for useInfiniteQuery to request the first group of data by default
    Waiting for useInfiniteQuery to request the first group of data by default
    useInfiniteQuery requests the first group of data by default The first page is fetched automatically before any interaction
    useInfiniteQuery requests the first group of data by default
    useInfiniteQuery
  • Returning the information for the next query in getNextPageParam
    Returning the information for the next query in getNextPageParam
    getNextPageParam returns the information for the next query It derives the next page param from the last page
    getNextPageParam returns the information for the next query
    getNextPageParam
  • Calling fetchNextPage function
    Calling fetchNextPage function
    fetchNextPage function loads the next page Calling it adds another page to the existing data set
    fetchNextPage function loads the next page
    fetchNextPage
import { useInfiniteQuery } from '@tanstack/react-query'

function Projects() {
  const fetchProjects = async ({ pageParam }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam)
    return res.json()
  }

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

  return status === 'pending' ? (
    <p>Loading...</p>
  ) : status === 'error' ? (
    <p>Error: {error.message}</p>
  ) : (
    <>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.data.map((project) => (
            <p key={project.id}>{project.name}</p>
          ))}
        </React.Fragment>
      ))}
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetching}
        >
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
              ? 'Load More'
              : 'Nothing more to load'}
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </>
  )
}

It's essential to understand that calling fetchNextPage while an ongoing fetch is in progress runs the risk of overwriting data refreshes happening in the background. This situation becomes particularly critical when rendering a list and triggering fetchNextPage simultaneously.It's essential to understand that calling fetchNextPage while an ongoing fetch is in progress runs the risk of overwriting data refreshes happening in the background. This situation becomes particularly critical when rendering a list and triggering fetchNextPage simultaneously.Calling fetchNextPage during an ongoing fetch risks overwriting refreshes This is most critical when rendering a list and triggering fetchNextPage togetherCalling fetchNextPage during an ongoing fetch risks overwriting refreshesfetchNextPage

Remember, there can only be a single ongoing fetch for an InfiniteQuery. A single cache entry is shared for all pages, attempting to fetch twice simultaneously might lead to data overwrites.Remember, there can only be a single ongoing fetch for an InfiniteQuery. A single cache entry is shared for all pages, attempting to fetch twice simultaneously might lead to data overwrites.Only one ongoing fetch exists per InfiniteQuery A single cache entry is shared for all pages, so parallel fetches may overwrite dataOnly one ongoing fetch exists per InfiniteQueryInfiniteQuery

If you intend to enable simultaneous fetching, you can utilize the { cancelRefetch: false } option (default: true) within fetchNextPage.If you intend to enable simultaneous fetching, you can utilize the { cancelRefetch: false } option (default: true) within fetchNextPage.cancelRefetch false enables simultaneous fetching in fetchNextPage The option defaults to true, which cancels an ongoing refetchcancelRefetch false enables simultaneous fetching in fetchNextPagecancelRefetch

To ensure a seamless querying process without conflicts, it's highly recommended to verify that the query is not in an isFetching state, especially if the user won't directly control that call.To ensure a seamless querying process without conflicts, it's highly recommended to verify that the query is not in an isFetching state, especially if the user won't directly control that call.Verify the query is not isFetching before fetching This avoids conflicts, especially when the user does not directly control the callVerify the query is not isFetching before fetchingisFetching

<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />

What happens when an infinite query needs to be refetched?

Stale infinite queries refetch each page sequentially from the first

When an infinite query becomes stale and needs to be refetched, each group is fetched sequentially, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested.When an infinite query becomes stale and needs to be refetched, each group is fetched sequentially, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested.Stale infinite queries refetch each page sequentially from the first This avoids stale cursors causing duplicates or skipped records, and cache removal restarts paginationStale infinite queries refetch each page sequentially from the firstsequential refetch

What if I want to implement a bi-directional infinite list?

Bi-directional lists use getPreviousPageParam and fetchPreviousPage

Bi-directional lists can be implemented by using the getPreviousPageParam, fetchPreviousPage, hasPreviousPage and isFetchingPreviousPage properties and functions.Bi-directional lists can be implemented by using the getPreviousPageParam, fetchPreviousPage, hasPreviousPage and isFetchingPreviousPage properties and functions.Bi-directional lists use getPreviousPageParam and fetchPreviousPage hasPreviousPage and isFetchingPreviousPage support the backward directionBi-directional lists use getPreviousPageParam and fetchPreviousPagegetPreviousPageParam

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})

What if I want to show the pages in reversed order?

Reverse pages with the select option

Sometimes you may want to show the pages in reversed order. If this is case, you can use the select option:Sometimes you may want to show the pages in reversed order. If this is case, you can use the select option:Reverse page order with the select option It reverses both the pages and pageParams arraysReverse page order with the select optionselect

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  select: (data) => ({
    pages: [...data.pages].reverse(),
    pageParams: [...data.pageParams].reverse(),
  }),
})

What if I want to manually update the infinite query?

Manually removing first page:

queryClient.setQueryData(['projects'], (data) => ({
  pages: data.pages.slice(1),
  pageParams: data.pageParams.slice(1),
}))

Manually removing a single value from an individual page:

const newPagesArray =
  oldPagesArray?.pages.map((page) =>
    page.filter((val) => val.id !== updatedId),
  ) ?? []

queryClient.setQueryData(['projects'], (data) => ({
  pages: newPagesArray,
  pageParams: data.pageParams,
}))

Keep only the first page:

Keep only the first page via setQueryData

queryClient.setQueryData(['projects'], (data) => ({
  pages: data.pages.slice(0, 1),
  pageParams: data.pageParams.slice(0, 1),
}))

Make sure to always keep the same data structure of pages and pageParams!Make sure to always keep the same data structure of pages and pageParams!Always keep the same data structure of pages and pageParams Manual updates must preserve both arrays for the query to workAlways keep the same data structure of pages and pageParamspages

What if I want to limit the number of pages?

maxPages limits stored pages to cut memory and network use

In some use cases you may want to limit the number of pages stored in the query data to improve the performance and UX:In some use cases you may want to limit the number of pages stored in the query data to improve the performance and UX:Limiting stored pages can improve performance and UX Fewer pages reduce memory use and the cost of refetchingLimiting stored pages can improve performance and UXperformance

  • when the user can load a large number of pages (memory usage)
    when the user can load a large number of pages (memory usage)
    Loading many pages increases memory usage Each stored page stays in the query data
    Loading many pages increases memory usage
    memory usage
  • when you have to refetch an infinite query that contains dozens of pages (network usage: all the pages are sequentially fetched)
    when you have to refetch an infinite query that contains dozens of pages (network usage: all the pages are sequentially fetched)
    Refetching dozens of pages consumes network via sequential fetches All pages are fetched sequentially
    Refetching dozens of pages consumes network via sequential fetches
    network usage

The solution is to use a "Limited Infinite Query". This is made possible by using the maxPages option in conjunction with getNextPageParam and getPreviousPageParam to allow fetching pages when needed in both directions.The solution is to use a "Limited Infinite Query". This is made possible by using the maxPages option in conjunction with getNextPageParam and getPreviousPageParam to allow fetching pages when needed in both directions.maxPages with getNextPageParam and getPreviousPageParam enables limited queries It allows fetching pages in both directions while capping stored pagesmaxPages with getNextPageParam and getPreviousPageParam enables limited queriesmaxPages

In the following example only 3 pages are kept in the query data pages array. If a refetch is needed, only 3 pages will be refetched sequentially.In the following example only 3 pages are kept in the query data pages array. If a refetch is needed, only 3 pages will be refetched sequentially.maxPages 3 keeps only 3 pages and refetches only 3 A refetch then sequentially fetches just the kept pagesmaxPages 3 keeps only 3 pages and refetches only 3maxPages

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
  maxPages: 3,
})

What if my API doesn't return a cursor?

Without a cursor, use pageParam as the cursor

If your API doesn't return a cursor, you can use the pageParam as a cursor. Because getNextPageParam and getPreviousPageParam also get the pageParamof the current page, you can use it to calculate the next / previous page param.If your API doesn't return a cursor, you can use the pageParam as a cursor. Because getNextPageParam and getPreviousPageParam also get the pageParamof the current page, you can use it to calculate the next / previous page param.Without a cursor, use pageParam as the cursor getNextPageParam and getPreviousPageParam also receive the current pageParamWithout a cursor, use pageParam as the cursorpageParam

return useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage, allPages, lastPageParam) => {
    if (lastPage.length === 0) {
      return undefined
    }
    return lastPageParam + 1
  },
  getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
    if (firstPageParam <= 1) {
      return undefined
    }
    return firstPageParam - 1
  },
})

Further reading

See the article How Infinite Queries work

To get a better understanding of how Infinite Queries work under the hood, see the article How Infinite Queries work.To get a better understanding of how Infinite Queries work under the hood, see the article How Infinite Queries work.See How Infinite Queries work for internals The article explains the mechanism under the hoodSee How Infinite Queries work for internalsHow Infinite Queries work