Data Fetching

Caching & Data Reuse

Instant navigation with cached data reuse between pages using supported APIs.

The Goal

Use already-loaded query data to make navigation feel instant, while still fetching fresh data in the background.

Supported Pattern (No Extra Cache API)

This library does not expose useConvexCached / useConvexData.

Instead, use:

  • useConvexQuery(...)
  • default for immediate placeholder/cached values
  • useNuxtData(...) to read existing useAsyncData cache entries
  • getQueryKey(...) to compute the exact cache key used by useConvexQuery

getQueryKey is intentionally not auto-imported. Import it explicitly from better-convex-nuxt/composables.

For a copy-pasteable end-to-end version, see the recipe:

You can also try the playground demo route:

  • /labs/query-features/cache-reuse

Example: List -> Detail Instant Navigation

List Page (populates cache)

<script setup lang="ts">
import { api } from '~~/convex/_generated/api'

const { data: posts } = await useConvexQuery(api.posts.list, {})
</script>

<template>
  <div>
    <NuxtLink v-for="post in posts" :key="post._id" :to="`/posts/${post.slug}`">
      {{ post.title }}
    </NuxtLink>
  </div>
</template>

Detail Page (reads cached list data, then fetches full post)

<script setup lang="ts">
import { api } from '~~/convex/_generated/api'
import { getQueryKey } from 'better-convex-nuxt/composables'

const route = useRoute()
const slug = computed(() => route.params.slug as string)

const postsCacheKey = getQueryKey(api.posts.list, {})
const { data: cachedPosts } = useNuxtData<
  Array<{
    _id: string
    slug: string
    title: string
    description?: string
    thumbnail?: string
  }>
>(postsCacheKey)

const { data: post, pending } = await useConvexQuery(
  api.posts.getBySlug,
  computed(() => ({ slug: slug.value })),
  {
    default: () => cachedPosts.value?.find((p) => p.slug === slug.value),
  },
)
</script>

<template>
  <div v-if="pending && !post">Loading...</div>

  <article v-else-if="post">
    <h1>{{ post.title }}</h1>
    <img v-if="post.thumbnail" :src="post.thumbnail" :alt="post.title" />
    <p v-if="post.description">{{ post.description }}</p>

    <div v-if="post.content">{{ post.content }}</div>
    <div v-else>Loading full content...</div>
  </article>
</template>

How It Works

  1. useConvexQuery(api.posts.list, {}) stores results in Nuxt async-data cache.
  2. getQueryKey(api.posts.list, {}) computes the same cache key.
  3. useNuxtData(key) reads that cached data synchronously.
  4. The detail page uses default to render cached fields instantly.
  5. useConvexQuery allows fresh detail data to load in the background.

Cache Key Rules

Cache reuse is opportunistic and depends on an exact query+args match.

  • Same query + same args => same cache key
  • Same query + different args => different cache key
  • If no prior query populated the cache, useNuxtData(...) returns undefined

Use getQueryKey(...) as the source of truth instead of manually constructing key strings.

Best Practices

  • Use useConvexQuery on detail pages to avoid navigation blocking.
  • Keep list queries lightweight (exclude heavy fields like full content).
  • Handle cache misses gracefully (pending && !post).
  • Apply the same shaping logic to cached defaults and fetched data when needed.

Important Note

Older docs/examples may mention useConvexCached or useConvexData. Those APIs are not part of the supported public surface.