<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 fetchQuery instead.Arguments can be reactive. The query re-executes when they change:
const categoryId = ref('tech')
const { data } = await useConvexQuery(
api.posts.byCategory,
computed(() => ({ categoryId: categoryId.value }))
)
// Changing categoryId triggers a new query
categoryId.value = 'design'
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 skipped (args = 'skip') |
'pending' | Waiting for data |
'success' | Data received |
'error' | Query failed |
By default, await useConvexQuery() blocks navigation until data loads. Use lazy: true to render immediately with a loading state:
const { data, pending } = await useConvexQuery(
api.posts.list,
{},
{ lazy: true }
)
<template>
<div v-if="pending">Loading...</div>
<PostList v-else :posts="data" />
</template>
For convenience, you can use useLazyConvexQuery() which is equivalent to useConvexQuery(..., { lazy: true }):
// These are equivalent:
const query1 = useLazyConvexQuery(api.posts.list, {})
const query2 = useConvexQuery(api.posts.list, {}, { lazy: true })
Use lazy: true for non-critical data that shouldn't block navigation:
<script setup lang="ts">
// Critical: blocks navigation
const { data: post } = await useConvexQuery(
api.posts.get,
computed(() => ({ id: route.params.id }))
)
// Non-critical: loads in background
const { data: related, pending } = useLazyConvexQuery(
api.posts.getRelated,
computed(() => ({ postId: route.params.id }))
)
</script>
<template>
<article>
<h1>{{ post?.title }}</h1>
<div v-html="post?.content" />
</article>
<aside>
<h2>Related Posts</h2>
<div v-if="pending">Loading...</div>
<PostCard v-else v-for="p in related" :key="p._id" :post="p" />
</aside>
</template>
By default, queries run on the client only. Use server: true for SSR data fetching:
// Enable SSR - useful for SEO-critical content
const { data } = await useConvexQuery(
api.posts.get,
{ id },
{ server: true }
)
See Core Concepts for all loading strategy combinations.
Queries subscribe to real-time updates by default. Use subscribe: false for data that doesn't need live updates:
const { data, refresh } = await useConvexQuery(
api.config.getSettings,
{},
{ subscribe: false }
)
// Data won't update automatically
// Use refresh() to manually re-fetch
await refresh()
When to use subscribe: false:
refresh() on search)Use 'skip' to conditionally disable a query:
const { isAuthenticated } = useConvexAuth()
const { data: profile, status } = await useConvexQuery(
api.users.getProfile,
computed(() => isAuthenticated.value ? {} : 'skip')
)
// status === 'idle' when skipped
Transform data after fetching to add computed fields:
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!
The transform runs on:
refresh() callsTransform can change the data shape. TypeScript infers the new type:
const { data } = await useConvexQuery(
api.posts.list,
{},
{
transform: (posts) => posts?.map(p => ({
...p,
formattedDate: formatDate(p.publishedAt)
}))
}
)
// data.value?.[0].formattedDate is typed as string
Provide placeholder data while loading:
const { data: posts } = await useConvexQuery(
api.posts.list,
{},
{ default: () => [] }
)
// posts is [] while loading, then real data
Re-fetch data on demand:
const { data, refresh } = await useConvexQuery(api.posts.list, {})
async function handleRefresh() {
await refresh()
}
Reset to initial state:
const { data, clear } = await useConvexQuery(api.posts.list, {})
function handleClear() {
clear() // data = null, status = 'idle'
}
Skip auth token overhead for public data:
const { data } = await useConvexQuery(
api.posts.featured,
{},
{ public: true }
)
Full type inference from your Convex schema:
import { 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 | MaybeRef<Args | 'skip'> | Query arguments (reactive) or 'skip' to disable |
options | UseConvexQueryOptions | Optional configuration |
| Option | Type | Default | Description |
|---|---|---|---|
lazy | boolean | false | Don't block navigation, load in background |
server | boolean | false | 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 |
public | boolean | false | Skip auth checks for public queries |
| Property | Type | Description |
|---|---|---|
data | Ref<T | null> | Query result (null when skipped or not yet loaded) |
status | ComputedRef<QueryStatus> | '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 |
execute | () => Promise<void> | Alias for refresh |
clear | () => void | Reset state to initial values |