Skip to main content

Loading and stale data

Render every query state the same way during SSR, argument changes, skips, and errors.

Use status for the main lifecycle and isStale when deliberately keeping an older same-identity result.

State meanings

StatusDataMeaning
idleundefinedSkipped, manual, or anonymous required query
pendingundefined or retained valueCurrent result is loading
successQuery result, including nullThe query finished loading successfully
errorUsually undefinedCurrent execution failed

pending is exactly equivalent to status === 'pending'.

An idle query is not running. blockedBy tells why: 'skip', 'manual' for an immediate: false query, or 'auth' for an anonymous auth: 'required' query. See why a query is not running.

Render all states

vue
<template>
  <ProjectSkeleton v-if="status === 'pending' && data === undefined" />

  <section v-else-if="status === 'error'">
    <p>Projects could not be loaded.</p>
    <button @click="refresh">Try again</button>
  </section>

  <p v-else-if="status === 'idle'">Select an organization.</p>

  <section v-else :aria-busy="isStale">
    <ProjectList :projects="data ?? []" />
    <p v-if="isStale">Updating results…</p>
  </section>
</template>

Do not use a truthiness check as the loading contract. A successful query can return null, while a stale query can be pending with data still visible.

Presentation fallbacks

Keep placeholders in presentation code so query state continues to describe only real query results:

ts
const projects = useConvexQuery(api.projects.list, {})
const visibleProjects = computed(() => projects.data.value ?? [])

Errors after previous data

When a refresh fails, use error and isStale to label any retained value as degraded rather than presenting it as current without context. When the signed-in user changes, the composable clears the previous user's data automatically.