Pagination
Load a Convex paginated query with SSR, live pages, filters, and explicit pagination status.
Use useConvexPaginatedQuery for a collection that grows beyond one bounded result.
Backend query
import { paginationOptsValidator } from 'convex/server'
import { v } from 'convex/values'
import { query } from './_generated/server'
export const list = query({
args: {
channelId: v.id('channels'),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
return await ctx.db
.query('messages')
.withIndex('by_channel_created', (q) => q.eq('channelId', args.channelId))
.order('desc')
.paginate(args.paginationOpts)
},
})Do not pass paginationOpts from the component. The composable owns cursors and page size.
Component
const { results, status, isLoading, isStale, hasNextPage, loadMore, error, refresh, reset } =
await useConvexPaginatedQuery(
api.messages.list,
{ channelId },
{ initialNumItems: 20, auth: 'required' },
)<template>
<MessageList :messages="results" :aria-busy="isStale" />
<button v-if="hasNextPage" :disabled="status === 'loading-more'" @click="loadMore(20)">
{{ status === 'loading-more' ? 'Loading…' : 'Load more' }}
</button>
<p v-if="error">Could not load messages.</p>
</template>Status values
| Status | Meaning |
|---|---|
idle | Skipped or anonymous required query |
loading-first-page | Initial page or refresh is loading |
ready | Current pages loaded; another page is available |
loading-more | A later page is loading |
exhausted | Convex reported no next page |
error | Current page operation failed |
Call loadMore() only when hasNextPage is true and the status is not already loading-more.
SSR and live pages
The first page can render during SSR. After hydration, each loaded page can have a live subscription. Unloaded pages are not present and cannot receive local live updates.
Filters and stale results
Use reactive arguments and keepPreviousData: true to retain old pages while the first page for a new filter loads. isStale marks that transition. Identity changes still clear all pages.
refresh() reloads the current pagination from the first page. reset() clears and starts the pagination controller again.
See infinite scroll for observer cleanup and accessible fallback controls.