Guide

useAction

useAction is a Vue composable for executing type-safe server actions with reactive status, validation errors, and lifecycle callbacks.

useAction

useAction is a Vue composable that wraps action execution with reactive state tracking, callbacks, and error handling.

Basic Usage

app/components/GreetForm.vue
<script setup lang="ts">
import { greet } from '#safe-action/actions'

const { execute, data, isExecuting } = useAction(greet)
</script>

<template>
  <button @click="execute({ name: 'World' })" :disabled="isExecuting">
    {{ isExecuting ? 'Loading...' : 'Greet' }}
  </button>
  <p v-if="data">{{ data.greeting }}</p>
</template>

With Callbacks

Pass callbacks as the second argument to react to lifecycle events:

<script setup lang="ts">
import { createPost } from '#safe-action/actions'

const { execute, data, isExecuting, hasSucceeded } = useAction(createPost, {
  onSuccess({ data, input }) {
    console.log('Created post:', data.title)
    // Navigate, show toast, etc.
  },
  onError({ error, input }) {
    console.error('Failed:', error)
  },
  onSettled({ result, input }) {
    // Runs after every execution, success or error
  },
  onExecute({ input }) {
    // Runs when execution starts
  },
})
</script>

Async Execution

Use executeAsync when you need to await the result:

<script setup lang="ts">
import { createPost } from '#safe-action/actions'

const { executeAsync } = useAction(createPost)

async function handleSubmit() {
  const result = await executeAsync({ title: 'Hello', body: 'World' })
  if (result.data) {
    navigateTo(`/posts/${result.data.id}`)
  }
}
</script>

Handling Validation Errors

validationErrors contains per-field error arrays returned by Zod validation:

<script setup lang="ts">
import { createPost } from '#safe-action/actions'

const { execute, validationErrors } = useAction(createPost)
</script>

<template>
  <form @submit.prevent="execute({ title: '', body: '' })">
    <div>
      <input placeholder="Title" />
      <span v-if="validationErrors?.title" class="error">
        {{ validationErrors.title[0] }}
      </span>
    </div>
    <div>
      <textarea placeholder="Body" />
      <span v-if="validationErrors?.body" class="error">
        {{ validationErrors.body[0] }}
      </span>
    </div>
    <button type="submit">Create</button>
  </form>
</template>

Resetting State

Call reset() to return all reactive state to its initial values:

<script setup lang="ts">
import { createPost } from '#safe-action/actions'

const { execute, data, reset, hasSucceeded } = useAction(createPost)
</script>

<template>
  <div v-if="hasSucceeded">
    <p>Post created: {{ data?.title }}</p>
    <button @click="reset()">Create Another</button>
  </div>
  <form v-else @submit.prevent="execute({ title: 'Hello', body: 'World' })">
    <button type="submit">Create Post</button>
  </form>
</template>

Detached Mode (Navigation-Safe Fetches)

By default, useAction uses Nuxt's $fetch internally. This works great for most cases, but $fetch is tied to the component lifecycle. If the user navigates away during an in-flight request, the request gets aborted and you see DOMException or NetworkError in the console.

This is a problem for background saves and auto-save systems where you fire a save and don't need to wait for the response.

Set detached: true to switch from $fetch to the browser's native fetch(). Native fetch() is not tied to the component lifecycle, so it keeps running even after the component that started it is destroyed.

<script setup lang="ts">
import { saveDocument } from '#safe-action/actions'

const { executeAsync, data, serverError } = useAction(saveDocument, {
  detached: true,
  onSuccess({ data }) {
    console.log('Saved:', data)
  },
  onError({ error }) {
    console.error('Save failed:', error)
  },
})

// This save will complete even if the user navigates away
async function autoSave(payload: DocumentPayload) {
  await executeAsync(payload)
}
</script>

Everything else works the same: reactive refs (data, serverError, status), callbacks (onSuccess, onError, onSettled), and the ActionResult return type are all identical to the default mode.

When to use detached mode:

  • Auto-save systems where the user might navigate away mid-save
  • Background operations that should complete regardless of component lifecycle
  • Any fire-and-forget mutation where you don't need to block the UI

When NOT to use detached mode:

  • Form submissions where you need to show inline validation errors (the component needs to stay mounted)
  • Operations where you need to redirect based on the result (use the default mode and await instead)

Status Tracking

The status ref tracks the current execution state:

<script setup lang="ts">
import { createPost } from '#safe-action/actions'

const { execute, status, isIdle, isExecuting, hasSucceeded, hasErrored } = useAction(createPost)
</script>

<template>
  <div>
    <p>Status: {{ status }}</p>
    <p v-if="isIdle">Ready to submit</p>
    <p v-if="isExecuting">Submitting...</p>
    <p v-if="hasSucceeded">Done!</p>
    <p v-if="hasErrored">Something went wrong</p>
  </div>
</template>
Copyright © 2026