Mutations
Execute transactional Convex writes with a mutate function and readonly state.
useConvexMutation returns a mutate function and readonly state refs. Destructure the parts you use:
const { mutate: updateProject, pending, error } = useConvexMutation(api.projects.update)
await updateProject({ projectId, name })Rename mutate to the action it performs. Rename the refs too when one component has several calls, for example pending: saving.
To keep each mutation as one object instead, wrap it in reactive():
<script setup lang="ts">
const { projectId } = defineProps<{ projectId: Id<'projects'> }>()
const create = reactive(useConvexMutation(api.projects.create))
const remove = reactive(useConvexMutation(api.projects.remove))
</script>
<template>
<button :disabled="create.pending" @click="create.mutate({ name: 'New' })">Create</button>
<button :disabled="remove.pending" @click="remove.mutate({ projectId })">Delete</button>
</template>State
| Field | Meaning |
|---|---|
mutate | Run the mutation; resolves with its result |
status | idle, pending, success, or error |
pending | Whether the newest call is pending |
data | Result of the newest successful call, or undefined |
error | Newest committed ConvexCallError, or undefined |
reset() | Return to idle and clear data and error |
data and error hold the exact values of the newest call. They are not deep reactive proxies.
Handle the rejected Promise
mutate rejects with a ConvexCallError. Catch it when the failure needs a decision:
import { isConvexCallError } from '@lupinum/better-convex-nuxt/errors'
try {
await updateProject({ projectId, name })
} catch (error) {
if (isConvexCallError(error, 'PROJECT_ARCHIVED')) {
toast.error(error.message)
return
}
throw error
}error.message is the text the Convex function put in ConvexError data, for example Archived projects cannot be edited. error.code is its data.code, and error.functionName is the mutation path, for example projects:update. See error types.
For a validated form, use useConvexForm to turn expected validation and mutation failures into a typed result. Run navigation, toast, or other follow-up work explicitly after await; neither composable runs callbacks for you.
Reset the state
Call reset() to clear a shown result or error, for example when a dialog closes:
const { mutate: renameProject, error: renameError, reset } = useConvexMutation(api.projects.rename)
function closeDialog() {
reset()
open.value = false
}reset() stops a call that has not been sent yet, for example one that still waits for sign-in to finish: it rejects with CANCELLED and outcome: 'not-sent', and is never sent. A call already sent still resolves or rejects its own Promise, but it no longer changes data, error, status, or pending. It cannot roll back a mutation that Convex already accepted.
Live query updates
After the mutation commits, Convex updates affected query subscriptions. Do not call every query's refresh() as a standard mutation step.
Use an optimistic update only when waiting for the confirmed live result creates a meaningful UX problem.
Identity changes
Each call remembers the signed-in user at the moment it starts. If a different user signs in, or the user signs out, before the call finishes, the call rejects with IDENTITY_CHANGED. Its result never appears in the new user's state.
When calls overlap, only the newest call updates data, error, status,
and pending. Every call still resolves or rejects its own Promise.
error.outcome tells whether the call was sent. With 'not-sent', the call never reached Convex. With 'unknown', the mutation may already have committed. Do not retry an identity-changed write automatically. See Idempotent writes.
Unavailable client
A call needs the browser Convex client. During server rendering, mutate rejects with code CLIENT_UNAVAILABLE. Call it from event handlers or onMounted(). A call made after the owning component unmounts rejects with code CANCELLED and changes no state.