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
| Status | Data | Meaning |
|---|---|---|
idle | undefined | Skipped, manual, or anonymous required query |
pending | undefined or retained value | Current result is loading |
success | Query result, including null | The query finished loading successfully |
error | Usually undefined | Current 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
<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:
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.