Pagination
Load a Convex paginated query with immediate state, SSR, live pages, and explicit 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
initialNumItems is required. Nuxt returns the reactive state immediately and makes that same return value optionally awaitable:
const messages = useConvexPaginatedQuery(
api.messages.list,
{ channelId },
{ initialNumItems: 20, auth: 'required' },
)
// Optional: wait for initial settlement when surrounding setup requires it.
const settled = await messages<template>
<MessageList :messages="messages.data.value ?? []" :aria-busy="messages.isStale.value" />
<button
v-if="messages.canLoadMore.value"
:disabled="messages.isLoading.value"
@click="messages.loadMore(20)"
>
{{ messages.isLoading.value ? 'Loading…' : 'Load more' }}
</button>
<p v-if="messages.error.value">Could not load messages.</p>
</template>The awaited value is a separate non-Promise view of the same refs. Initial success, error, skip, and server: false resolve; inspect its state for the outcome.
Returned state
| Field | Meaning |
|---|---|
data | Flattened loaded items, or undefined before a page exists |
status | idle, pending, success, or error |
isLoading | true while the current page operation is pending |
canLoadMore | true when Convex reports another page |
error | Normalized ConvexCallError, or undefined |
isStale | Same-identity previous pages are visible for new arguments |
loadMore(n) | Request the next page with n items |
refresh() | Retire loaded pages and load again from the first page |
Call loadMore() only when canLoadMore.value is true and isLoading.value is false.
Options
The Nuxt options are exactly initialNumItems, auth, keepPreviousData, and server. Plain Vue uses the same options without server.
Use reactive arguments and keepPreviousData: true to retain old pages while the first page for a new filter loads. isStale marks that same-identity transition. Identity changes always clear every page.
Page ownership
The first page can render during SSR. After hydration, the controller keeps loaded page boundaries live and handles Convex page-split signals internally. Applications consume the flattened data and never manage cursors, page metadata, hydration seeds, or first-page settlement hooks.
See infinite scroll for observer cleanup and accessible fallback controls.