Data Fetching

Fetching Data

Query data with SSR support, real-time subscriptions, and reactive state management.

Basic Usage

<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.

Client vs Server: useConvexQuery is for Vue components. For server routes (server/api/), use serverConvexQuery instead.

Execution Model

useConvexQuery is async and blocking by design. Always await it:

PatternWhat it doesBest for
const { data } = await useConvexQuery(...)Resolves query state before setup continuesDefault usage in pages and components
const { data } = await useConvexQuery(..., { server: false })Skips SSR fetch, then blocks on client fetchClient-only data that must not be rendered into SSR HTML

Why useConvexQuery Instead of useAsyncData?

useAsyncData is excellent for HTTP fetches, but Convex queries need a hybrid model:

  1. SSR-first fetch for payload hydration.
  2. Seamless client upgrade to real-time WebSocket subscriptions.
  3. Shared query cache + subscription dedupe across components.

useConvexQuery wraps these behaviors in one Nuxt-native composable while keeping the standard pending/status/error/refresh ergonomics.


Passing Arguments

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, {})

Reactive Arguments

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)

Loading States

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>
StatusMeaning
'idle'Query is disabled (enabled: false or args are null/undefined)
'pending'Waiting for data
'success'Data received
'error'Query failed
Troubleshooting status mismatch (error in UI, success in DevTools):
  1. Ensure your module version and lockfile are up to date.
  2. Clear Nuxt build artifacts (pnpm nuxt cleanup) and restart dev.
  3. Confirm query args are valid (prefer getter args for reactive inputs).
  4. If used in route middleware, switch to useConvexCall (client) or serverConvexQuery (server). ::

Progressive UI Pattern

If you want the page shell to render quickly while secondary data resolves, move that secondary query into a child component and wrap it in <Suspense>.
<template>
  <article>
    <MainPost />
  </article>

  <aside>
    <Suspense>
      <RelatedPosts />
      <template #fallback>
        <div>Loading related posts...</div>
      </template>
    </Suspense>
  </aside>
</template>

SSR Behavior

By default, queries run on the server during SSR (like Nuxt's 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 })),
)

Disable SSR for Specific Queries

For client-only data or performance optimization, disable SSR:
const { data, status } = await useConvexQuery(
  api.posts.getBySlug,
  computed(() => ({ slug: route.params.slug as string })),
  { server: false }, // Client-only, shows loading state on refresh
)

Configure Global Defaults

Change the default for all queries in nuxt.config.ts:
export default defineNuxtConfig({
  convex: {
    defaults: {
      server: false, // Disable SSR globally
    },
  },
})
See SSR & Hydration for all loading strategy combinations.

Real-time Control

Queries subscribe to real-time updates by default. Use 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()
When to use subscribe: false:
  • Configuration/settings that rarely change
  • Historical data or reports
  • Reducing WebSocket connections
  • Search results (use refresh() on search)
For route middleware/guards, do not use query composables. Use useConvexCall (client middleware) or serverConvexQuery (server middleware) instead.

Disabling Queries

Use nullable args and/or 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

Transform Data

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:
  • SSR fetch result
  • Every WebSocket subscription update
  • Manual refresh() calls

Type-Safe Transforms

Transform 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

Default Data

Provide placeholder data while loading:
const { data: posts } = await useConvexQuery(api.posts.list, {}, { default: () => [] })

// posts is [] while loading, then real data
If you provide transform, the default() value is passed through transform too.

Manual Refresh

Re-fetch data on demand:
const { data, refresh } = await useConvexQuery(api.posts.list, {})

async function handleRefresh() {
  await refresh()
}

One-Shot Decision Guide

Use this quick rule:
  1. Rendering reactive UI data: use useConvexQuery.
  2. Rendering UI data without WebSocket updates: use useConvexQuery(..., { subscribe: false }).
  3. Imperative event calls: use useConvexCall.
  4. Route middleware calls: use useConvexCall on client navigation and serverConvexQuery on SSR navigation.

Clear State

Reset to initial state:
const { data, clear } = await useConvexQuery(api.posts.list, {})

function handleClear() {
  clear() // clears current asyncData payload/error state
}

Auth Mode

Control auth token attachment for queries:
const { data } = await useConvexQuery(api.posts.featured, {}, { auth: 'none' })
  • auth: 'auto' (default): attach token when available
  • auth: 'none': never attach auth token

TypeScript

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

API Reference

Parameters

ParameterTypeDescription
queryFunctionReference<"query">Convex query function reference
argsMaybeRefOrGetter<Args | null | undefined>Query arguments (reactive); null/undefined disables
optionsUseConvexQueryOptionsOptional configuration

Options

OptionTypeDefaultDescription
serverbooleantrueRun query on server during SSR
subscribebooleantrueSubscribe to real-time updates via WebSocket
default() => TundefinedFactory for initial/placeholder data
transform(raw: RawT) => TundefinedTransform data after fetching
auth'auto' | 'none''auto'Auth token mode ('none' skips token attachment)
enabledMaybeRefOrGetter<boolean>trueExplicitly enable/disable the query
keepPreviousDatabooleanfalseKeep last settled data during arg transitions
deepUnrefArgsbooleantrueDeeply unwrap refs inside args objects/arrays
All options can be configured globally via convex.defaults in nuxt.config.ts. Per-query options override global defaults.

Return Values

PropertyTypeDescription
dataRef<T | null>Query result (null when skipped or not yet loaded)
statusComputedRef<ConvexCallStatus>'idle' | 'pending' | 'success' | 'error'
pendingComputedRef<boolean>Shorthand for status === 'pending'
errorRef<Error | null>Error if query failed
refresh() => Promise<void>Re-fetch data via HTTP
clear() => voidReset state to initial values