Form submissions
Validate external form values and submit one typed Convex mutation.
useConvexForm is a submission controller, not a form framework. Your component or form library controls values, touched and dirty state, field arrays, focus, navigation, and messages. useConvexForm validates the values with a Standard Schema and sends them to one Convex mutation.
import { type } from 'arktype'
const checkpointSchema = type({
balance: 'number > 0',
note: 'string',
})
const { submit, pending, fieldErrors, formError } = useConvexForm(api.accounts.createCheckpoint, {
schema: checkpointSchema,
toArgs: (values) => ({
balanceCents: Math.round(values.balance * 100),
note: values.note || undefined,
}),
mapError: (error) => ({
form: error.kind === 'server' ? error.message : 'The checkpoint could not be saved.',
}),
})
async function save() {
const result = await submit(
{ balance: balance.value, note: note.value },
{ accountId: accountId.value },
)
if (!result.ok) return
toast.success('Checkpoint saved.')
navigateTo(`/accounts/${accountId.value}`)
}The schema is required. It may validate asynchronously or transform values. toArgs receives the parsed output; any mutation arguments it does not produce become the typed second argument to submit().
Render errors
<template>
<form @submit.prevent="save">
<input v-model.number="balance" :aria-invalid="Boolean(fieldErrors.balance)" />
<p v-for="message in fieldErrors.balance" :key="message">
{{ message }}
</p>
<p v-if="formError" role="alert">{{ formError }}</p>
<button :disabled="pending">Save</button>
</form>
</template>Known top-level issue paths become field errors. Nested paths retain their complete path and route to the matching top-level field. Pathless and unknown paths remain visible as form errors.
mapError receives the safe ConvexCallError. It may map a domain error to known submitted fields or a form message. Without mapError, the form message is error.message. For a server error, that is the text your Convex function put in ConvexError data. Backend validation and authorization remain canonical; browser validation is only faster feedback.
Submission rules
- Expected validation and mutation failures resolve as
{ ok: false, error }. - Success resolves as
{ ok: true, data }. - A second
submit()while one is pending rejects with aConvexCallErrorwith codeSUBMIT_IN_PROGRESSand sends no mutation. Disable the submit button whilependingistrue. reset()returns toidle, clearsdataanderror, and lets a new submission start at once. It cannot cancel or roll back a mutation already accepted by Convex.- An identity change, such as signing out, works like
reset(): the form returns toidleand clears the previous user'sdata,error, and field messages at once. - A reset, identity change, or component disposal prevents an older completion from repopulating state. The older submission still resolves or rejects its own Promise.
- A submission that is retired while its schema or
toArgsstill runs never sends the mutation. It resolves{ ok: false, error }. The code isIDENTITY_CHANGEDwhen the signed-in user changed, andCANCELLEDafterreset()or component disposal. - A submission belongs to the user who called
submit(). When that user changes while the mutation runs, the submission resolves{ ok: false, error }with codeIDENTITY_CHANGED, anderror.callError.outcomeis'unknown': the mutation may have saved. error.callError.outcomeis'not-sent'when the mutation was never sent. See Error types.
Use useConvexMutation directly for concurrent calls, optimistic writes, uploads, autosave, or commands that are not forms. Use useConvexOperation for several mutations that belong together. Use a dedicated Vue form library when you need field registration, touched/dirty state, field arrays, or wizard behavior; pass its values to submit().