---
title: "Mutations & Actions"
description: "Write with useMutation, reach outside Convex with useAction, and make writes feel instant with optimistic updates that roll back on their own."
canonical_url: "https://nuxt-convex-module.dev/guide/mutations-and-actions"
---
# Mutations & Actions

> Write with useMutation, reach outside Convex with useAction, and make writes feel instant with optimistic updates that roll back on their own.

Queries subscribe; mutations and actions are one-shot calls. Both composables return a **stable plain function** — no `.value`, safe to pass to children or watchers — so the call site reads like an ordinary async function.

```ts
const send = useMutation(api.messages.send)
await send({ body: 'hi' })

const checkout = useAction(api.payments.checkout)
```

A **mutation** writes to the database transactionally. An **action** can run in the Node runtime and call external services, so it is not reactive and cannot be subscribed to.

## `useAction()`

Unlike queries, actions are **not reactive subscriptions** — `useAction` returns a plain async function that resolves once per call. The demo below computes text stats and a SHA-256 hash with ~600ms of artificial latency, so watch the pending state:

<playground-analyze>



</playground-analyze>

```vue [components/Analyzer.vue]
<script setup lang="ts">
import { api } from '#convex/api'

const analyze = useAction(api.analyze.text)

const input = ref('')
const result = ref()
const error = ref<string>()
const analyzing = ref(false)

async function submit() {
  error.value = undefined
  analyzing.value = true
  try {
    result.value = await analyze({ input: input.value })
  }
  catch (e) {
    error.value = e instanceof Error ? e.message : String(e)
  }
  finally {
    analyzing.value = false
  }
}
</script>
```

Because an action is just a promise, you own the in-flight and error states: a `ref` flipped around `await` covers pending, and `try/catch` catches server-side failures (empty input here returns zeros, but an external API being down would land in the same `catch`).

## Optimistic updates

Chain `.withOptimisticUpdate()` onto the function returned by `useMutation` to update local query results before the server responds. The handler must be synchronous, receives the client's local query store and the mutation args, and is rolled back automatically once the mutation completes and the real results sync. It returns a **new** `VueMutation` — and throws if the mutation already has an update configured, so call it once per `useMutation`, not per invocation.

```ts
import { api } from '#convex/api'

const toggle = useMutation(api.tasks.toggle).withOptimisticUpdate(
  (localStore, args) => {
    const current = localStore.getQuery(api.tasks.get, { id: args.id })
    if (current !== undefined) {
      localStore.setQuery(api.tasks.get, { id: args.id }, {
        ...current,
        completed: !current.completed,
      })
    }
  },
)
```

The placeholder values only exist locally — when the server result syncs back, the optimistic write is rolled back and replaced by the authoritative row (or simply rolled back if the mutation throws).

Optimistic updates over a **paginated** query need to find the right page first; four helpers do that for you — see [pagination](https://nuxt-convex-module.dev/guide/pagination#optimistic-updates-over-pages).


## Sitemap

See the full [sitemap](https://nuxt-convex-module.dev/sitemap.md) for all pages.
