Skip to main content
Fetch paginated or infinite scrolling data with the useInfiniteQuery composable. It manages multiple pages of data and provides methods to load more pages.

Signature

function useInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
  options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
  queryClient?: QueryClient,
): UseInfiniteQueryReturnType<TData, TError>

Parameters

options
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
required
Configuration options for the infinite query. Can be a reactive ref or getter function.
queryClient
QueryClient
Custom QueryClient instance. If not provided, uses the client from context.

Returns

UseInfiniteQueryReturnType<TData, TError>
object
Reactive refs containing infinite query state and methods.

Type Parameters

  • TQueryFnData - Type of data returned by each page
  • TError - Type of error (defaults to DefaultError)
  • TData - Type of final data (defaults to InfiniteData<TQueryFnData>)
  • TQueryKey - Type of the query key (defaults to QueryKey)
  • TPageParam - Type of page parameter (defaults to unknown)

Examples

Basic Infinite Scroll

<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: async ({ pageParam }) => {
    const res = await fetch(`/api/posts?page=${pageParam}`)
    return res.json()
  },
  initialPageParam: 0,
  getNextPageParam: (lastPage, allPages) => {
    return lastPage.hasMore ? allPages.length : undefined
  },
})
</script>

<template>
  <div>
    <div v-for="page in data?.pages" :key="page.id">
      <div v-for="post in page.posts" :key="post.id">
        <h3>{{ post.title }}</h3>
        <p>{{ post.body }}</p>
      </div>
    </div>
    
    <button
      @click="fetchNextPage()"
      :disabled="!hasNextPage || isFetchingNextPage"
    >
      <span v-if="isFetchingNextPage">Loading more...</span>
      <span v-else-if="hasNextPage">Load More</span>
      <span v-else>No more posts</span>
    </button>
  </div>
</template>

Cursor-Based Pagination

<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: async ({ pageParam }) => {
    const res = await fetch(`/api/posts?cursor=${pageParam}`)
    return res.json()
  },
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
</script>

Bi-directional Pagination

<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'

const {
  data,
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: async ({ pageParam }) => {
    const res = await fetch(`/api/posts?cursor=${pageParam}`)
    return res.json()
  },
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor,
})
</script>

<template>
  <div>
    <button @click="fetchPreviousPage()" :disabled="!hasPreviousPage">
      Load Previous
    </button>
    
    <!-- Posts display -->
    
    <button @click="fetchNextPage()" :disabled="!hasNextPage">
      Load Next
    </button>
  </div>
</template>

With TypeScript

<script setup lang="ts">
import { useInfiniteQuery } from '@tanstack/vue-query'

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

interface PostsPage {
  posts: Post[]
  nextCursor?: number
  hasMore: boolean
}

const { data } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: async ({ pageParam }): Promise<PostsPage> => {
    const res = await fetch(`/api/posts?cursor=${pageParam}`)
    return res.json()
  },
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// data.pages is typed as PostsPage[]
</script>

Reactive Page Parameters

<script setup>
import { ref } from 'vue'
import { useInfiniteQuery } from '@tanstack/vue-query'

const filter = ref('all')

const { data, fetchNextPage } = useInfiniteQuery({
  queryKey: ['posts', filter], // Refetches when filter changes
  queryFn: async ({ pageParam }) => {
    const res = await fetch(
      `/api/posts?filter=${filter.value}&cursor=${pageParam}`
    )
    return res.json()
  },
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
</script>

<template>
  <select v-model="filter">
    <option value="all">All</option>
    <option value="active">Active</option>
    <option value="completed">Completed</option>
  </select>
</template>

Infinite Scroll with Intersection Observer

<script setup>
import { ref, watch } from 'vue'
import { useInfiniteQuery } from '@tanstack/vue-query'

const loadMoreRef = ref(null)

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// Auto-load more when element is visible
watch(loadMoreRef, (el) => {
  if (!el) return
  
  const observer = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting && hasNextPage.value && !isFetchingNextPage.value) {
        fetchNextPage()
      }
    },
    { threshold: 1.0 }
  )
  
  observer.observe(el)
})
</script>

<template>
  <div>
    <div v-for="page in data?.pages" :key="page.id">
      <div v-for="post in page.posts" :key="post.id">
        {{ post.title }}
      </div>
    </div>
    
    <div ref="loadMoreRef" v-if="hasNextPage">
      <span v-if="isFetchingNextPage">Loading...</span>
    </div>
  </div>
</template>

Refetch All Pages

<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'

const { data, refetch } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

const refreshAll = () => {
  // Refetches all loaded pages
  refetch()
}
</script>

Build docs developers (and LLMs) love