Now that you have a basic query set up, let's explore what makes better-convex-nuxt special: Granular control over data loading.
We'll cover client-side loading, Server-Side Rendering (SSR), and mutations.
To demonstrate the loading states effectively, we need a multi-page app. Let's restructure our application slightly.
<script setup lang="ts">
import { api } from '~~/convex/_generated/api'
// Default behavior: SSR enabled (like Nuxt's useFetch)
const { data: tasks, error } = await useConvexQuery(api.tasks.get, {})
</script>
<template>
<NuxtLink to="/">Back Home</NuxtLink>
<div>
<p v-if="error">Error: {{ error.message }}</p>
<ul v-else>
<li v-for="task in tasks" :key="task._id">{{ task.text }}</li>
</ul>
</div>
</template>
Test it out:
/query. Data loads instantly - no loading spinner!/ (Home) and back to /query. Navigation waits for data.By default, useConvexQuery runs on the server during SSR - just like Nuxt's useFetch. This means:
Trade-off: SSR triggers two function calls to Convex: one HTTP fetch on the server, then a WebSocket subscription on the client. For high-traffic apps, consider the impact on your Convex bill.
For non-critical data or client-only interactions, disable SSR:
const { data, status } = await useConvexQuery(api.tasks.get, {}, {
server: false // Client-only, shows loading state on refresh
})
To change the default behavior for all queries in your app:
export default defineNuxtConfig({
convex: {
url: process.env.CONVEX_URL,
defaults: {
server: false, // Disable SSR globally
lazy: true // Enable lazy loading globally
}
}
})
{ lazy: true } to render on the server for the first visit, but show a loading spinner immediately on client-side navigation instead of blocking.With SSR enabled by default, better-convex-nuxt hydrates the state and establishes a WebSocket connection on the client. This means your app remains real-time.
Let's add a mutation to create new tasks.
First, update your backend logic:
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const get = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("tasks").collect();
},
});
// Add this mutation
export const create = mutation({
args: {
text: v.string(),
},
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", {
text: args.text,
isCompleted: false,
});
},
});
Update app/pages/query.vue to include a form. We will use useConvexMutation to handle the interaction.
<script setup lang="ts">
import { api } from '~~/convex/_generated/api'
const { data: tasks, status, error } = await useConvexQuery(api.tasks.get, {})
// Setup mutation
const { mutate, pending, error: mutationError, reset } = useConvexMutation(api.tasks.create)
const taskText = ref('')
async function handleSubmit() {
if (!taskText.value.trim()) return
// Execute mutation
await mutate({ text: taskText.value })
taskText.value = ''
}
</script>
<template>
<NuxtLink to="/">Home</NuxtLink>
<div>
<!-- Form Section -->
<form @submit.prevent="handleSubmit">
<input v-model="taskText" type="text" placeholder="Enter task..." :disabled="pending" />
<button type="submit" :disabled="pending || !taskText.trim()">
{{ pending ? 'Creating...' : 'Create Task' }}
</button>
</form>
<div v-if="mutationError">
<p>Something went wrong.</p>
<button @click="reset">Dismiss</button>
</div>
<!-- List Section -->
<p v-if="status === 'pending'">Loading...</p>
<ul v-else>
<li v-for="task in tasks" :key="task._id">{{ task.text }}</li>
</ul>
</div>
</template>
Test it out: Add a task. It appears immediately! Open the app in a second browser window. When you add a task in one window, it instantly appears in the other.
Sometimes you don't want a WebSocket connection (e.g., for static blog posts or expensive queries). You can disable subscriptions and control updates manually.
Change your query options:
const { data, refresh } = await useConvexQuery(api.tasks.get, {}, {
subscribe: false // Disable real-time updates
})
And add a refresh button to your template:
<button @click="() => refresh()">Refresh List</button>
Test it out: Add a task. The list will not update. Click "Refresh List" to fetch the new data manually.
You have mastered the basics! Continue with authentication or explore advanced topics.