useAction
useAction
useAction is a Vue composable that wraps action execution with reactive state tracking, callbacks, and error handling.
Basic Usage
<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
awaitinstead)
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>