Skip to main content
Fetch paginated or infinite scrolling data with the createInfiniteQuery function. It manages multiple pages of data and provides methods to load more pages in Svelte 5 applications.

Signature

function createInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
  options: Accessor<CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>>,
  queryClient?: Accessor<QueryClient>,
): CreateInfiniteQueryResult<TData, TError>

Parameters

options
Accessor<CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>>
required
A function (accessor) returning infinite query configuration options.
queryClient
Accessor<QueryClient>
Accessor returning a custom QueryClient instance. If not provided, uses the client from context.

Returns

CreateInfiniteQueryResult<TData, TError>
object
Reactive 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 (Svelte 5 Runes)

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

const query = createInfiniteQuery(() => ({
  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
  },
}))

function loadMore() {
  query.fetchNextPage()
}
</script>

{#each query.data?.pages ?? [] as page}
  {#each page.posts as post}
    <div>
      <h3>{post.title}</h3>
      <p>{post.body}</p>
    </div>
  {/each}
{/each}

<button 
  onclick={loadMore}
  disabled={!query.hasNextPage || query.isFetchingNextPage}
>
  {#if query.isFetchingNextPage}
    Loading more...
  {:else if query.hasNextPage}
    Load More
  {:else}
    No more posts
  {/if}
</button>

Cursor-Based Pagination

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

const query = createInfiniteQuery(() => ({
  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 lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

const query = createInfiniteQuery(() => ({
  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>

<button 
  onclick={() => query.fetchPreviousPage()}
  disabled={!query.hasPreviousPage || query.isFetchingPreviousPage}
>
  Load Previous
</button>

<!-- Posts display -->

<button 
  onclick={() => query.fetchNextPage()}
  disabled={!query.hasNextPage || query.isFetchingNextPage}
>
  Load Next
</button>

With TypeScript

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

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

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

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

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

Reactive Query Parameters

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

let filter = $state('all')

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

<select bind:value={filter}>
  <option value="all">All</option>
  <option value="active">Active</option>
  <option value="completed">Completed</option>
</select>

Infinite Scroll with Intersection Observer

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'
import { onMount } from 'svelte'

let loadMoreRef: HTMLDivElement

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

onMount(() => {
  const observer = new IntersectionObserver(
    ([entry]) => {
      if (
        entry.isIntersecting && 
        query.hasNextPage && 
        !query.isFetchingNextPage
      ) {
        query.fetchNextPage()
      }
    },
    { threshold: 1.0 }
  )

  if (loadMoreRef) {
    observer.observe(loadMoreRef)
  }

  return () => observer.disconnect()
})
</script>

<div>
  {#each query.data?.pages ?? [] as page}
    {#each page.posts as post}
      <div>{post.title}</div>
    {/each}
  {/each}
  
  {#if query.hasNextPage}
    <div bind:this={loadMoreRef}>
      {#if query.isFetchingNextPage}
        Loading...
      {/if}
    </div>
  {/if}
</div>

Refetch All Pages

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

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

function refreshAll() {
  query.refetch() // Refetches all loaded pages
}
</script>

<button onclick={refreshAll}>Refresh All</button>

With Loading States

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

const query = createInfiniteQuery(() => ({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
}))
</script>

{#if query.isLoading}
  <div>Loading first page...</div>
{:else if query.isError}
  <div>Error: {query.error?.message}</div>
{:else if query.isSuccess}
  {#each query.data.pages as page}
    {#each page.posts as post}
      <div>{post.title}</div>
    {/each}
  {/each}
  
  {#if query.hasNextPage}
    <button 
      onclick={() => query.fetchNextPage()}
      disabled={query.isFetchingNextPage}
    >
      {query.isFetchingNextPage ? 'Loading...' : 'Load More'}
    </button>
  {/if}
{/if}

Page-Based Pagination

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

const query = createInfiniteQuery(() => ({
  queryKey: ['posts'],
  queryFn: async ({ pageParam }) => {
    const res = await fetch(`/api/posts?page=${pageParam}&limit=10`)
    return res.json()
  },
  initialPageParam: 1,
  getNextPageParam: (lastPage, allPages, lastPageParam) => {
    return lastPage.hasMore ? lastPageParam + 1 : undefined
  },
  getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
    return firstPageParam > 1 ? firstPageParam - 1 : undefined
  },
}))
</script>

Search with Infinite Scroll

<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

let searchTerm = $state('')
let debouncedSearch = $state('')

// Debounce search
$effect(() => {
  const timeout = setTimeout(() => {
    debouncedSearch = searchTerm
  }, 300)
  
  return () => clearTimeout(timeout)
})

const query = createInfiniteQuery(() => ({
  queryKey: ['posts', 'search', debouncedSearch],
  queryFn: async ({ pageParam }) => {
    const res = await fetch(
      `/api/posts?search=${debouncedSearch}&cursor=${pageParam}`
    )
    return res.json()
  },
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  enabled: debouncedSearch.length > 0,
}))
</script>

<input 
  type="text" 
  bind:value={searchTerm}
  placeholder="Search posts..."
/>

{#if query.data}
  {#each query.data.pages as page}
    {#each page.posts as post}
      <div>{post.title}</div>
    {/each}
  {/each}
{/if}

Notes

Svelte Query uses Svelte 5’s runes mode. The options parameter must be an accessor (function) to track reactive dependencies.
initialPageParam is required in v5. Make sure to provide it when creating infinite queries.
When using refetch(), all pages will be refetched. For better UX, consider using invalidateQueries to only refetch when needed.

Build docs developers (and LLMs) love