---
url: /guide/infinite-queries.md
---
# Infinite Queries

Infinite queries let you load and merge multiple pages into a **single cache entry**.

This is different from paginated queries with `useQuery()` where each page is a separate cache entry (page is part of the `key`).

If you haven’t read it yet, start with [Paginated Queries](./paginated-queries.md).

## When to use

`useInfiniteQuery()` isn't for every use case. It’s ideal when:

* you want infinite scrolling / "load more" behavior (most common)
* you want all loaded pages to be invalidated/refetched as one unit
* you don't want pages to be garbage-collected independently

## Key design

With infinite queries, **the page/cursor is not part of the query key**.

Put *filters* in the key (search text, sort order, user id…), but not the `pageParam`:

```ts
useInfiniteQuery({
  key: () => ['feed', { search: search.value }],
  // ...
})
```

Changing the key resets the infinite query as it creates a new cache entry.

## Page-based example

```ts
import { useInfiniteQuery } from '@pinia/colada'

const {
  data,
  asyncStatus,
  hasNextPage,
  loadNextPage,
} = useInfiniteQuery({
  key: ['feed'],
  initialPageParam: 1,
  query: ({ pageParam }) => fetch(`/api/feed?page=${pageParam}`).then(r => r.json()),
  getNextPageParam: (lastPage) => lastPage.nextPage ?? null,
})
```

The result is shaped like:

* `data.value.pages`: array of pages
* `data.value.pageParams`: the associated params used to fetch each page

Each time you call `loadNextPage()`, the new page is merged into the same cache entry as the key is the same.

To make it work, you must define how to get the next page param with `getNextPageParam` (or `getPreviousPageParam` for backward pagination). In this example, we use the key `nextPage` from the latest response.

So, the API returns something like:

```json
{
  "items": [/* page items */],
  "nextPage": 2
}
```

When `nextPage` is `null`, `hasNextPage` becomes `false` and `loadNextPage()` does nothing.

## Cursor-based example

If your API returns a cursor, you can use that as the page param instead of a page number:

```ts
useInfiniteQuery({
  key: ['notifications'],
  initialPageParam: null as string | null,
  query: ({ pageParam }) => api.listNotifications({ cursor: pageParam }),
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? null,
})
```

## Load more example

You may want a feed-like behavior where you load more items as the user scrolls down. This can be achieved using an `IntersectionObserver` that triggers `loadNextPage()` when the user scrolls near the bottom of the list.

```vue twoslash
<script setup lang="ts">
import { useTemplateRef, watch, onWatcherCleanup } from 'vue'
import { useInfiniteQuery } from '@pinia/colada'

const {
  data: facts,
  loadNextPage,
  hasNextPage,
} = useInfiniteQuery({
  key: ['feed'],
  query: async ({ pageParam }) => fetch(`/api/feed?page=${pageParam}&limit=10`).then(r => r.json()),
  initialPageParam: 1,
  getNextPageParam: (lastPage) => (lastPage.nextPageUrl ? lastPage.currentPage + 1 : null),
})

const loadMoreTrigger = useTemplateRef<HTMLElement>('loadMoreTrigger')

watch(loadMoreTrigger, (el) => {
  if (!el) return
  const observer = new IntersectionObserver(
    (entries) => {
      if (entries[0]?.isIntersecting) {
        loadNextPage()
      }
    },
    {
      rootMargin: '300px',
      threshold: [0],
    },
  )
  observer.observe(el)
  onWatcherCleanup(() => {
    observer.disconnect()
  })
})
</script>

<template>
  <ul>
    <li v-for="item in (facts?.pages ?? []).flatMap(page => page.items)" :key="item.id">
      {{ item.text }}
    </li>
  </ul>
  <div ref="loadMoreTrigger" v-if="hasNextPage">Loading more...</div>
</template>
```

## Limiting memory with `maxPages`

For very long feeds (or chat-like UIs), you can keep only a fixed number of pages in memory:

```ts
useInfiniteQuery({
  key: ['chat', conversationId],
  initialPageParam: 1,
  maxPages: 10,
  // ...
})
```

This will keep only the 10 most recent pages in memory. When a new page is loaded, the oldest page is removed from the cache.

## Invalidation and refetch

Invalidating the infinite query invalidates the **whole entry**. Refetching will refetch the loaded pages.

## SSR

SSR works like any other query: the infinite query is serialized as one entry. Typically you’ll prefetch the initial page, and optionally load more pages server-side if your UI requires it.
