<script setup lang="ts">
import { api } from '~~/convex/_generated/api'
// Fetch posts with SSR and real-time updates
const { data: posts } = await useConvexQuery(api.posts.list, {})
</script>
<template>
<PostCard v-for="post in posts" :key="post._id" :post="post" />
</template>
The await blocks navigation until data loads. Data is fetched on the server during SSR, then syncs in real-time via WebSocket.
useConvexQuery is for Vue components. For server routes (server/api/), use serverConvexQuery instead.useConvexQuery is async and blocking by design. Always await it:
| Pattern | What it does | Best for |
|---|---|---|
const { data } = await useConvexQuery(...) | Resolves query state before setup continues | Default usage in pages and components |
const { data } = await useConvexQuery(..., { server: false }) | Skips SSR fetch, then blocks on client fetch | Client-only data that must not be rendered into SSR HTML |
useConvexQuery Instead of useAsyncData?useAsyncData is excellent for HTTP fetches, but Convex queries need a hybrid model:
useConvexQuery wraps these behaviors in one Nuxt-native composable while keeping the standard pending/status/error/refresh ergonomics.
The second parameter is the arguments object. Pass values that match your Convex query's args:
// Convex query definition
export const getById = query({
args: { id: v.id('posts') },
handler: async (ctx, args) => {
return await ctx.db.get(args.id)
},
})
// In your Vue component
const { data: post } = await useConvexQuery(
api.posts.getById,
{ id: 'abc123' }, // Pass the required args
)
For queries with no arguments, pass an empty object {}:
const { data: posts } = await useConvexQuery(api.posts.list, {})
When arguments come from reactive sources (refs, route params), pass a getter function. The query re-executes automatically when values change:
// From route params
const route = useRoute()
const { data: post } = await useConvexQuery(api.posts.getBySlug, () => ({
slug: route.params.slug as string,
}))
// From a ref
const categoryId = ref('tech')
const { data } = await useConvexQuery(api.posts.byCategory, () => ({
categoryId: categoryId.value,
}))
// Changing categoryId triggers a new query
categoryId.value = 'design'
You can also pass a ref or computed directly when that better fits your component setup.
// Also valid: explicit computed wrapper
const filters = reactive({ categoryId: 'tech' })
const safeArgs = computed(() => ({ categoryId: filters.categoryId }))
const query = await useConvexQuery(api.posts.byCategory, safeArgs)
Use status to handle all query states:
<script setup lang="ts">
const { data: posts, status, error } = await useConvexQuery(api.posts.list, {})
</script>
<template>
<div v-if="status === 'idle'">Select a category</div>
<div v-else-if="status === 'pending'">Loading...</div>
<div v-else-if="status === 'error'">{{ error?.message }}</div>
<div v-else-if="status === 'success' && !posts?.length">No posts</div>
<div v-else>
<PostCard v-for="post in posts" :key="post._id" :post="post" />
</div>
</template>
| Status | Meaning |
|---|---|
'idle' | Query is disabled (enabled: false or args are null/undefined) |
'pending' | Waiting for data |
'success' | Data received |
'error' | Query failed |
error in UI, success in DevTools):pnpm nuxt cleanup) and restart dev.useConvexCall (client) or serverConvexQuery (server).
::<Suspense>.<template>
<article>
<MainPost />
</article>
<aside>
<Suspense>
<RelatedPosts />
<template #fallback>
<div>Loading related posts...</div>
</template>
</Suspense>
</aside>
</template>
useFetch). This means no loading spinners on initial page load.const route = useRoute()
// Default behavior - SSR enabled
const { data } = await useConvexQuery(
api.posts.getBySlug,
computed(() => ({ slug: route.params.slug as string })),
)
const { data, status } = await useConvexQuery(
api.posts.getBySlug,
computed(() => ({ slug: route.params.slug as string })),
{ server: false }, // Client-only, shows loading state on refresh
)
nuxt.config.ts:export default defineNuxtConfig({
convex: {
defaults: {
server: false, // Disable SSR globally
},
},
})
subscribe: false for data that doesn't need live updates.
This is an HTTP-only mode (no WebSocket dependency for initial fetches or refreshes):const { data, refresh } = await useConvexQuery(api.config.getSettings, {}, { subscribe: false })
// Data won't update automatically
// Use refresh() to manually re-fetch
await refresh()
subscribe: false:refresh() on search)useConvexCall (client middleware) or serverConvexQuery (server middleware) instead.enabled to conditionally disable a query:const { isAuthenticated } = useConvexAuth()
const { data: profile, status } = await useConvexQuery(
api.users.getProfile,
() => (isAuthenticated.value ? {} : undefined),
{ enabled: () => isAuthenticated.value },
)
// status === 'idle' when disabled
const { data: posts } = await useConvexQuery(
api.posts.list,
{},
{
transform: (posts) =>
posts?.map((post) => ({
...post,
formattedDate: new Date(post.publishedAt).toLocaleDateString(),
readingTime: Math.ceil(post.content.split(' ').length / 200) + ' min read',
excerpt: post.content.slice(0, 150) + '...',
})),
},
)
// posts now has formattedDate, readingTime, excerpt fields!
refresh() callsconst { data } = await useConvexQuery(
api.posts.list,
{},
{
transform: (posts) =>
posts?.map((p) => ({
...p,
formattedDate: formatDate(p.publishedAt),
})),
},
)
// data.value?.[0].formattedDate is typed as string
const { data: posts } = await useConvexQuery(api.posts.list, {}, { default: () => [] })
// posts is [] while loading, then real data
transform, the default() value is passed through transform too.const { data, refresh } = await useConvexQuery(api.posts.list, {})
async function handleRefresh() {
await refresh()
}
useConvexQuery.useConvexQuery(..., { subscribe: false }).useConvexCall.useConvexCall on client navigation and serverConvexQuery on SSR navigation.const { data, clear } = await useConvexQuery(api.posts.list, {})
function handleClear() {
clear() // clears current asyncData payload/error state
}
const { data } = await useConvexQuery(api.posts.featured, {}, { auth: 'none' })
auth: 'auto' (default): attach token when availableauth: 'none': never attach auth tokenimport { api } from '~~/convex/_generated/api'
// Args and return type are inferred
const { data: posts } = await useConvexQuery(api.posts.list, {})
// posts is Ref<Post[] | null>
posts.value?.map((post) => post.title) // post is typed
| Parameter | Type | Description |
|---|---|---|
query | FunctionReference<"query"> | Convex query function reference |
args | MaybeRefOrGetter<Args | null | undefined> | Query arguments (reactive); null/undefined disables |
options | UseConvexQueryOptions | Optional configuration |
| Option | Type | Default | Description |
|---|---|---|---|
server | boolean | true | Run query on server during SSR |
subscribe | boolean | true | Subscribe to real-time updates via WebSocket |
default | () => T | undefined | Factory for initial/placeholder data |
transform | (raw: RawT) => T | undefined | Transform data after fetching |
auth | 'auto' | 'none' | 'auto' | Auth token mode ('none' skips token attachment) |
enabled | MaybeRefOrGetter<boolean> | true | Explicitly enable/disable the query |
keepPreviousData | boolean | false | Keep last settled data during arg transitions |
deepUnrefArgs | boolean | true | Deeply unwrap refs inside args objects/arrays |
convex.defaults in nuxt.config.ts. Per-query options override global defaults.| Property | Type | Description |
|---|---|---|
data | Ref<T | null> | Query result (null when skipped or not yet loaded) |
status | ComputedRef<ConvexCallStatus> | 'idle' | 'pending' | 'success' | 'error' |
pending | ComputedRef<boolean> | Shorthand for status === 'pending' |
error | Ref<Error | null> | Error if query failed |
refresh | () => Promise<void> | Re-fetch data via HTTP |
clear | () => void | Reset state to initial values |