---
title: "Infinite scroll"
description: "A paginated Convex list that loads the next page as the reader reaches the end."
canonical_url: "https://nuxt-convex-module.dev/recipes/infinite-scroll"
---
# Infinite scroll

> A paginated Convex list that loads the next page as the reader reaches the end.

`usePaginatedQuery` gives you `results`, a `status` and a stable `loadMore(n)`. Infinite scroll is one sentinel element at the end of the list and an `IntersectionObserver` that calls `loadMore` when the sentinel comes into view.

```vue [pages/feed.vue]
<script setup lang="ts">
import { api } from '#convex/api'

const { results, status, isLoading, loadMore } = usePaginatedQuery(
  api.messages.list,
  {},
  { initialNumItems: 20 },
)

const sentinel = useTemplateRef('sentinel')

onMounted(() => {
  const observer = new IntersectionObserver(([entry]) => {
    if (entry?.isIntersecting && status.value === 'CanLoadMore') loadMore(20)
  }, { rootMargin: '200px' })
  watchEffect(() => sentinel.value && observer.observe(sentinel.value))
  onUnmounted(() => observer.disconnect())
})
</script>

<template>
  <ul>
    <li v-for="message in results" :key="message._id">{{ message.body }}</li>
  </ul>
  <div ref="sentinel" />
  <p v-if="isLoading">Loading…</p>
  <p v-else-if="status === 'Exhausted'">That's everything.</p>
</template>
```

- Guard on `status === 'CanLoadMore'`: while a page is in flight the status is `LoadingMore`, so a sentinel that stays visible doesn't fire twice.
- `rootMargin` starts the fetch before the sentinel is on screen; tune it to how fast your readers scroll.
- New rows written on the server appear in the loaded pages live — no refetch.

Full statuses, reactive args and optimistic updates over pages: [Pagination](https://nuxt-convex-module.dev/guide/pagination).


## Sitemap

See the full [sitemap](https://nuxt-convex-module.dev/sitemap.md) for all pages.
