Skip to main content

Composables

Options, return values, and errors of every Better Convex composable.

Nuxt auto-imports these composables. Import generated function references from #convex/api. A Vue application without Nuxt imports the same composables from @lupinum/better-convex-vue and its references from convex/_generated/api.

Every composable returns a plain object of refs and functions, like Nuxt's useFetch. Destructure it in <script setup>, so templates unwrap the refs. The functions do not use this.

To use the result as one object, wrap it in reactive(). Then read create.pending in the script and the template without .value:

ts
const create = reactive(useConvexMutation(api.tasks.create))
const tasks = reactive(await useConvexQuery(api.tasks.list)) // Nuxt: wrap the awaited result

Do not destructure a reactive() object. Destructured values do not update.

Call composables inside component setup or another Vue effect scope. When the scope ends, the composable stops its subscription and cancels its work.

Query APIs

useConvexQuery

ts
const { data, status, pending, error, isStale, blockedBy, execute, refresh } = useConvexQuery(
  query,
  args,
  options,
)
ArgumentType
queryA generated query reference, for example api.tasks.list
argsThe query arguments, 'skip', or a ref or getter that returns either. Each argument can also be a ref.
optionsUseNuxtConvexQueryOptions in Nuxt, UseConvexQueryOptions in Vue. Optional.

A query with an empty argument validator (args: {}) may omit args. A query that declares any argument needs args, even when every argument is optional. Pass 'skip' to pause the query.

OptionTypeDefaultEffect
auth'optional' | 'required' | 'none'defaultQueryAuthrequired waits for a signed-in user. optional runs for anyone. none always runs without a user.
keepPreviousDatabooleanfalseKeep the last result while new arguments load. isStale is true during that time.
immediatebooleantrueWith false, the query waits until you call execute().
serverbooleantrueNuxt only. With false, the query does not run during server rendering.
lazybooleanfalseNuxt only. With true, navigation does not wait for the result. You cannot combine it with immediate: false.

The default for auth is convex.auth.defaultQueryAuth in Nuxt and the defaultQueryAuth option of createBetterConvex in Vue. Both default to 'optional'.

ReturnTypeMeaning
dataComputedRef<Data | undefined>The query result. undefined means there is no result yet. null is a real result.
statusComputedRef<ConvexCallStatus>'idle', 'pending', 'success', or 'error'
pendingComputedRef<boolean>true while status is 'pending'
errorComputedRef<ConvexCallError | undefined>The last failure. error.functionName names the query.
isStaleComputedRef<boolean>data belongs to earlier arguments (keepPreviousData)
blockedByComputedRef<ConvexQueryBlockedBy>Why the query does not run, or null
execute()() => Promise<void>Start a query created with immediate: false
refresh()() => Promise<void>Run the query again

The return type is UseConvexQueryState<Data>.

blockedBy is 'skip' when the arguments are 'skip'. It is 'manual' when the query uses immediate: false and you did not call execute(). It is 'auth' when the query waits for authentication, needs a signed-in user, or has an authentication error. It is null when the query may run. The server render and the browser report the same value.

In Nuxt, the return value is also a Promise. await useConvexQuery(...) waits until the first result or error exists, then returns the same refs. It does not wait when the query is skipped, uses lazy: true, server: false, or immediate: false, or runs while the page hydrates. Query guide.

useConvexPaginatedQuery

ts
const {
  data,
  status,
  pending,
  error,
  isStale,
  blockedBy,
  canLoadMore,
  isLoadingMore,
  isExhausted,
  loadMore,
  execute,
  refresh,
  reset,
} = useConvexPaginatedQuery(query, args, { initialNumItems: 20 })

query must be a paginated query: it takes paginationOpts and returns a PaginationResult. args are the query arguments without paginationOpts, or 'skip'. Each argument can also be a ref.

OptionTypeDefaultEffect
initialNumItemsnumberRequiredSize of the first page. Must be a positive integer.
initialCursorstring | nullnullStart the list at this cursor
authConvexAuthModedefaultQueryAuthAs for useConvexQuery
keepPreviousDatabooleanfalseAs for useConvexQuery
immediatebooleantrueAs for useConvexQuery
serverbooleantrueNuxt only. As for useConvexQuery.
lazybooleanfalseNuxt only. As for useConvexQuery.

The return type is UseConvexPaginatedQueryState<Item>.

  • data is one read-only array of every loaded item, or undefined before the first page exists.
  • status and pending describe the first page only. Loading a later page, or a failed later page, leaves status at 'success'.
  • error is the first-page error. When the first page succeeded, it is the error of the failed later page. The items loaded before that page stay in data.
  • canLoadMore is true when the first page succeeded, the last loaded page is not the end of the list, and no later page is loading.
  • isLoadingMore is true while a later page loads.
  • isExhausted is true when the last loaded page is the end of the list.
  • loadMore(numItems) requests numItems more items and returns Promise<void>. The Promise resolves when the page loads, fails, or is replaced, or when the list resets or stops. It never rejects. When canLoadMore is false, the call does nothing. After a later page fails, loadMore() requests it again. It throws at once when numItems is not a positive integer.
  • reset(cursor?) drops every loaded page and starts again, at cursor when you pass one.
  • blockedBy, isStale, execute(), and refresh() work as for useConvexQuery.

In Nuxt, the server renders the first page only. The browser loads later pages. The return value is also a Promise, as for useConvexQuery. Pagination guide.

Write APIs

useConvexMutation

ts
const { mutate, data, status, pending, error, reset } = useConvexMutation(mutation, {
  optimisticUpdate,
})
ReturnMeaning
mutate()Run the mutation. Resolves with its result. Rejects with a ConvexCallError.
dataThe result of the newest successful call, or undefined
status'idle', 'pending', 'success', or 'error' for the newest call
pendingtrue while the newest call runs
errorThe ConvexCallError of the newest failed call, or undefined
reset()Return to 'idle' and clear data and error

Only the newest call changes the state. reset() stops every call that was not sent yet; it rejects with CANCELLED and outcome: 'not-sent'. A call that was already sent resolves or rejects its own Promise but no longer changes the state. A change of the signed-in user has the same effect. The return type is UseConvexMutationReturn<Mutation>.

optimisticUpdate(store, args) is optional. It must be synchronous. It receives Convex's OptimisticLocalStore, with getQuery, getAllQueries, and setQuery. Mutation guide.

useConvexAction

ts
const { run, data, status, pending, error, reset } = useConvexAction(action)

run(args) runs the action. It resolves with the result and rejects with a ConvexCallError. The state and reset() work as for useConvexMutation. The return type is UseConvexActionReturn<Action>. Action guide.

Failures raised by the library

mutate() and run() reject with these codes when the failure does not come from your Convex function:

CodeWhen
IDENTITY_CHANGEDThe signed-in user changed before the call finished. The write may have happened.
CANCELLEDreset() ran before the call was sent, or the call started after its scope ended
CLIENT_UNAVAILABLENo browser Convex client exists, for example during server rendering

Each of these errors has outcome: 'not-sent' when the call was never sent, or 'unknown' when it was sent and the user changed before its result arrived. When your Convex function throws a ConvexError, the error keeps your data.code and your message, and has no outcome. See error types.

useConvexOperation

ts
const { run, data, status, pending, error, reset } = useConvexOperation(
  async (op, documentId: Id<'documents'>, file: File) => {
    const uploadUrl = await op.mutation(api.files.generateUploadUrl)
    const storageId = await op.upload(uploadUrl, file)
    return op.mutation(api.documents.attachFile, { documentId, storageId })
  },
)

await run(documentId, file)

run(...args) starts an operation for the signed-in user at that moment and calls your function with the operation and args. It resolves with the function's result and rejects with a ConvexCallError. data, status, pending, and error work as for useConvexAction. When the signed-in user changes, the state returns to 'idle'. reset() returns to 'idle' and cancels every running operation. The return types are UseConvexOperationReturn<Args, Result> and ConvexOperation.

Operation memberMeaning
query(ref, args)Run a query once. Resolves with its result.
mutation(ref, args)Run a mutation. Resolves with its result.
action(ref, args)Run an action. Resolves with its result.
upload(url, file, options?)Send file to a Convex storage upload URL. Resolves with the storage ID. Options: onProgress, maxSize, allowedTypes.
retiredtrue after the operation stopped
signalAn AbortSignal that aborts when the operation stops while your function runs. Its reason is the ConvexCallError that stopped it.
cancel()Stop the operation. Later steps reject with CANCELLED and are not sent. A running upload() is aborted.

maxSize and allowedTypes work as in useConvexFileUpload: a file that fails them rejects with FILE_TOO_LARGE or FILE_TYPE_NOT_ALLOWED and outcome: 'not-sent', and no request is sent.

Each step checks the user immediately before it sends its request. Steps reject with a ConvexCallError:

  • IDENTITY_CHANGED with outcome: 'not-sent': the user changed before the step was sent.
  • IDENTITY_CHANGED with outcome: 'unknown': the user changed while the step was in flight.
  • CANCELLED: cancel(), reset(), or the end of the calling scope stopped the operation.
  • CLIENT_UNAVAILABLE: no browser Convex client exists, for example during server rendering.

A query, mutation, or action that was already sent finishes with its real result after cancel(). Multi-step operations guide.

useConvexForm

ts
const { submit, data, status, pending, error, issues, fieldErrors, formError, reset } =
  useConvexForm(mutation, { schema, toArgs, mapError })

const result = await submit(values, extraArgs)

useConvexForm validates form values with a Standard Schema validator, such as Zod or Valibot, and sends them to one mutation. It does not store the form values, touched or dirty state, or show messages. The return type is UseConvexFormReturn.

OptionRequiredEffect
schemaYesValidates the values that you pass to submit()
toArgsNoMaps the validated values to mutation arguments. Without it, the validated values must match the mutation arguments.
mapErrorNoMaps a ConvexCallError from the mutation to { form?, fields? } messages

submit(values, extraArgs) validates values, adds extraArgs, and runs the mutation. Pass extraArgs for mutation arguments that the form does not produce, such as a project ID. It is required only when those remaining arguments are required.

  • submit() resolves with { ok: true, data } or { ok: false, error }. Validation failures and mutation failures resolve. They do not reject.
  • A second submit() while one runs rejects with a ConvexCallError with code SUBMIT_IN_PROGRESS and sends nothing. Disable the submit button while pending is true.
  • submit() throws a TypeError when extraArgs repeats a key that the form produces.
  • reset() returns to 'idle', clears data and error, and lets a new submission start at once.
  • A submission retired during validation sends nothing and resolves { ok: false, error } with code IDENTITY_CHANGED (the signed-in user changed) or CANCELLED (reset() or disposal).

error is a ConvexFormError:

FieldTypeMeaning
kind'validation' | 'submission'The schema rejected the values, or the mutation failed
messagestringA summary message
issuesreadonly ConvexFormIssue[]Each issue has message, path, and an optional field
fieldErrorsReadonly<Record<string, readonly string[]>>Messages for each field
formErrorstring | undefinedA message for the whole form
callErrorConvexCallError | undefinedThe mutation error, for a submission error

The issues, fieldErrors, and formError refs return the same values as the fields of error, or empty values when there is no error. In Nuxt, import ConvexFormError from @lupinum/better-convex-nuxt/errors; in plain Vue, from @lupinum/better-convex-vue/errors. Form guide.

Authentication API

These APIs exist only when convex.auth is set in the Nuxt configuration.

useConvexAuth

ts
const { status, pending, user, error, client, ready } = useConvexAuth()
ReturnTypeMeaning
statusComputedRef<ConvexAuthStatus>'loading', 'anonymous', 'authenticated', or 'error'
pendingComputedRef<boolean>A Better Auth request, such as sign-in, is running. It does not follow status.
userReadonly<Ref<ConvexUser | null>>The signed-in user, or null
errorComputedRef<ConvexCallError | undefined>The last authentication error
clientBetter Auth clientThe Better Auth client, with the plugins from convex.auth.client. Never null.
ready()(options?: { timeoutMs?: number }) => Promise<ConvexAuthStatus>Wait until the first authentication check finishes, then return the status

ready() waits without a time limit by default. With timeoutMs, it waits at most that many milliseconds and then returns the current status.

ConvexUser has id, and optional name, email, emailVerified, image, createdAt, and updatedAt. The optional fields come from the Convex session token, which carries only the ID unless defineSessionClaims adds them. The server render sends the user with the page, so the first browser render shows the same user.

The real client exists only in the browser, after the Convex connection starts. Before that, and during server rendering, client is a placeholder. You can destructure it and read its properties. Its methods throw a ConvexCallError with code CLIENT_UNAVAILABLE. Call client methods from event handlers or onMounted().

A client method that changes the session returns only after Convex accepts the new session. Examples are sign-in, sign-out, two-factor verification, organization.setActive, and session revocation. Read-only methods, such as organization.list() or getSession(), return without that wait. Synchronous methods, such as client.useSession(), stay synchronous. The client does not expose $fetch, $store, or hydrateSession.

Render each state yourself:

vue
<script setup lang="ts">
const { status, error } = useConvexAuth()
</script>

<template>
  <AccountSkeleton v-if="status === 'loading'" />
  <AccountMenu v-else-if="status === 'authenticated'" />
  <SignInCard v-else-if="status === 'anonymous'" />
  <ErrorCard v-else :message="error?.message ?? 'Authentication failed'" />
</template>

Auth state guide.

useConvexAuthReturnTo

ts
const returnTo = useConvexAuthReturnTo() // ComputedRef<string | null>

Returns the redirect query value that the route middleware adds when it sends a visitor to the sign-in page. The value is a checked local path. It is null when the value is missing, repeated, or not a safe local path. Navigate to it after sign-in.

normalizeLocalRedirectPath

ts
normalizeLocalRedirectPath(value: unknown): string | null

Returns value as a local path that starts with one /. Returns null for any other value, including //host, /\host, absolute URLs, and control characters. useConvexAuthReturnTo uses the same check. Route protection guide.

Client and configuration

useConvex

ts
const convex = useConvex()

Returns one stable handle with query, mutation, action, and onUpdate. The type is ConvexClientHandle. The handle stays valid when the signed-in user changes.

The handle works like the Convex client. Its calls reject with the error that Convex raised, such as a ConvexError or Error, not with a ConvexCallError. Only a change of the signed-in user during a call rejects with a ConvexCallError with code IDENTITY_CHANGED, and its outcome tells whether the call was sent. Pass a caught error to normalizeConvexError before you read kind or code:

ts
try {
  await convex.mutation(api.notes.create, { title })
} catch (error) {
  const failure = normalizeConvexError(error, { functionName: 'notes:create' })
  if (isConvexCallError(failure, 'NOTE_LOCKED')) showLockedNotice()
}

In Nuxt, you can call useConvex() anywhere. During server rendering, or when no Convex URL is set, query, mutation, and action reject and onUpdate throws, each with a ConvexCallError with code CLIENT_UNAVAILABLE. Use useConvexQuery for server-rendered data and serverConvex(event) in Nitro routes. In Vue, useConvex() throws when the createBetterConvex() plugin is not installed.

useConvexAttachment

ts
const attachment = useConvexAttachment()

Nuxt only, browser only. Returns the BetterConvexAttachment of the current Nuxt application. Pass it to a separately built Vue application with createBetterConvex({ attachment }). That application then uses the same Convex connection and signed-in user. The attachment contains no tokens and no Better Auth controls. On the server, or when no Convex URL is set, it throws a ConvexCallError with code CLIENT_UNAVAILABLE.

useConvexConfig

ts
const { url, siteUrl } = useConvexConfig()

Nuxt only. Returns the Convex deployment URL and the HTTP actions URL (*.convex.site). Both are read-only strings or undefined. The type is ConvexRuntimeConfig.

useConvexConnectionState

ts
const { state, isConnected, isReconnecting, pendingMutations, pendingActions } =
  useConvexConnectionState()
ReturnTypeMeaning
stateComputedRef<ConnectionState>The Convex connection state from convex/browser
isConnectedComputedRef<boolean>The WebSocket is connected
isReconnectingComputedRef<boolean>A connection was lost and has not come back yet
pendingMutationsComputedRef<number>Mutations sent but not yet confirmed
pendingActionsComputedRef<number>Actions sent but not yet finished

During server rendering, and in a Nuxt build without a Convex URL, it reports a disconnected state. Use it for a connection indicator. Use query and mutation state to decide what the user can do. Connection state guide.

createBetterConvex

Vue only. Install the plugin once, at the application root:

src/main.ts
import { createBetterConvex } from '@lupinum/better-convex-vue'

app.use(createBetterConvex({ convexUrl: import.meta.env.VITE_CONVEX_URL }))
OptionTypeEffect
convexUrlstringThe Convex deployment URL. Required unless you pass attachment.
authBetterConvexAuthAdapterOptional. Connects your identity provider. See plain Vue.
clientOptionsBetterConvexClientOptionsOptional. verbose, webSocketConstructor, skipConvexDeploymentUrlCheck, and unsavedChangesWarning.
attachmentBetterConvexAttachmentUse the connection of a host application instead of convexUrl, auth, and clientOptions
defaultQueryAuth'optional' | 'required' | 'none'The auth mode for queries that do not set one. Default: 'optional'.

Any other clientOptions key throws a TypeError. unsavedChangesWarning defaults to false.

File API

useConvexFileUpload

ts
const { upload, data, status, pending, error, progress, cancel, reset } = useConvexFileUpload(
  api.files.generateUploadUrl,
  {
    maxSize: 5 * 1024 * 1024,
    allowedTypes: ['image/*'],
    complete: (
      op,
      { storageId, context: documentId }: UploadCompleteContext<string, Id<'documents'>>,
    ) => op.mutation(api.documents.attachFile, { documentId, storageId }),
  },
)

const { storageId, prepared, completed } = await upload(file, {}, { context: documentId })

The first argument is the prepare mutation. It returns an upload URL from ctx.storage.generateUploadUrl(), or an object that contains one.

OptionTypeEffect
maxSizenumberLargest allowed file, in bytes. A larger file fails before any request.
allowedTypesreadonly string[]Allowed MIME types, such as 'application/pdf' or 'image/*'
url(prepared, { file, context }) => stringSelects the upload URL from the prepare result. Required when that result is not a string.
complete(op, { prepared, storageId, file, context }) => Promise<Result>Runs after the file is stored. Send its Convex calls through op, a ConvexOperation. Its result becomes completed.

upload(file, args, { context }) runs prepare, upload, and complete, and resolves with { storageId, prepared, completed } (ConvexFileUploadResult). Pass args when the prepare mutation has arguments, and {} or undefined when it has none. context is a per-call value, such as the ID of the record to attach the file to, that url and complete receive as context. It is captured when upload() is called: plain objects and arrays, including reactive() state, are copied. Its type comes from the annotation on complete or url (UploadCompleteContext<Prepared, Context>); once declared, { context } is required unless the type includes undefined. data holds the result of the last successful upload. pending is true during all steps. progress has loaded, total, and percent of the storage request. The return type is UseConvexFileUploadReturn<Mutation, Completed, Context>.

  • cancel() stops the running upload, and the state returns to 'idle'. A step that was not sent is never sent, and the storage request is aborted. A Convex call that was already sent finishes. Without a running upload, cancel() does nothing.
  • reset() stops a running upload the same way and clears data, error, and progress.
  • When the component unmounts, the running upload stops in the same way.
  • When the signed-in user changes, the upload rejects with IDENTITY_CHANGED and sends no later step.

upload() rejects with a ConvexCallError. error.phase is the step that failed (prepare, upload, or complete), and error.outcome tells whether its request was sent.

CodeWhen
FILE_TOO_LARGEThe file is larger than maxSize. No request is sent.
FILE_TYPE_NOT_ALLOWEDThe MIME type does not match allowedTypes. No request is sent.
UPLOAD_IN_PROGRESSAnother upload of the same composable is running
CANCELLEDcancel(), reset(), or unmounting stopped the upload
IDENTITY_CHANGEDThe signed-in user changed during the upload
CLIENT_UNAVAILABLENo browser Convex client exists, for example during server rendering
INVALID_UPLOAD_URLThe prepare mutation, or url, did not give a non-empty URL

Other failures come from your Convex functions or the storage request. functionName names the Convex function that failed. After CANCELLED and IDENTITY_CHANGED, status is 'idle', not 'error'.

The composable uploads one file at a time. To upload several files, create your own list and call upload() for each file. Show a stored file with an ordinary useConvexQuery that returns ctx.storage.getUrl(storageId). File guide.