Skip to main content

Loading and Stale Data

Render deterministic query states 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 or anonymous required query
pendingundefined or retained valueCurrent result is loading
successQuery result, including nullCurrent result settled successfully
errorUsually undefinedCurrent execution failed

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

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. Identity transitions retire identity-owned data automatically.