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>

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