Concurrent operations
Represent overlapping mutations and actions without misleading shared loading state.
One mutation or action composable exposes state for its latest call lifecycle. Decide whether operations should share that state before reusing one instance.
One form, one operation state
import { normalizeConvexError, type ConvexCallError } from '@lupinum/better-convex-nuxt/errors'
const { mutate: saveSettings, pending: saving } = useConvexMutation(api.settings.update)
const formError = ref<ConvexCallError | null>(null)
async function submit() {
if (saving.value) return
try {
await saveSettings(form.value)
} catch (error) {
formError.value = normalizeConvexError(error)
}
}This fits a form where only one submission should be active.
Independent rows need independent state
For a list with per-row actions, create an explicit per-row component:
<script setup lang="ts">
import type { Id } from '~~/convex/_generated/dataModel'
import { api } from '#convex/api'
const props = defineProps<{ projectId: Id<'projects'> }>()
const { mutate: archiveProject, pending: archiving } = useConvexMutation(api.projects.archive)
</script>
<template>
<button :disabled="archiving" @click="archiveProject({ projectId: props.projectId })">
{{ archiving ? 'Archiving…' : 'Archive' }}
</button>
</template>Each component instance has its own operation state. One row does not disable every row.
Latest-call state
When calls overlap through one composable, only the latest call updates data, error, and status. Earlier calls still resolve or reject for the code that called them. After reset(), no call that is still running updates the state, and a call that was not sent yet is never sent: it rejects with CANCELLED.
Do not use one pending ref as an exact count of all network operations. Use separate instances or your own queue when the product must show every job.
Disable duplicate writes deliberately
Disable or deduplicate when repeating the operation would be harmful. Backend idempotency remains the reliable protection against retries, double clicks, and network ambiguity.
Identity transition
When the signed-in user changes, the composable clears its call state and ignores results of calls that are still running. Those calls reject with IDENTITY_CHANGED. Read error.outcome:
'unknown': the call was sent. The server may already have saved the write.'not-sent': the call was never sent. Nothing was saved.
For several calls that belong together, use useConvexOperation. It stops the remaining calls when the user changes.