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

Reactive Arguments

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'

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 skipped (args = 'skip')
'pending'Waiting for data
'success'Data received
'error'Query failed

Lazy Loading

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

When to Use Lazy Loading

Use lazy: true for non-critical data that shouldn't block navigation:

  • Secondary content (sidebar, recommendations)
  • Below-the-fold content
  • Data that can load after the main content

Example: Main + Secondary Content

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

SSR Behavior

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.


Real-time Control

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:

  • Configuration/settings that rarely change
  • Historical data or reports
  • Reducing WebSocket connections
  • Search results (use refresh() on search)

Skipping Queries

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

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

Manual Refresh

Re-fetch data on demand:

const { data, refresh } = await useConvexQuery(api.posts.list, {})

async function handleRefresh() {
  await refresh()
}

Clear State

Reset to initial state:

const { data, clear } = await useConvexQuery(api.posts.list, {})

function handleClear() {
  clear() // data = null, status = 'idle'
}

Public Queries

Skip auth token overhead for public data:

const { data } = await useConvexQuery(
  api.posts.featured,
  {},
  { public: true }
)

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
argsMaybeRef<Args | 'skip'>Query arguments (reactive) or 'skip' to disable
optionsUseConvexQueryOptionsOptional configuration

Options

OptionTypeDefaultDescription
lazybooleanfalseDon't block navigation, load in background
serverbooleanfalseRun 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
publicbooleanfalseSkip auth checks for public queries

Return Values

PropertyTypeDescription
dataRef<T | null>Query result (null when skipped or not yet loaded)
statusComputedRef<QueryStatus>'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
execute() => Promise<void>Alias for refresh
clear() => voidReset state to initial values