Optimistic Updates
Optimistic updates
useOptimisticAction extends useAction. Provide a writable currentData ref and an immutable updateFn(input, current) that returns a new value. It changes currentData immediately and exposes a readonly optimisticData view.
<script setup lang="ts">
import { ref } from 'vue'
import { toggleTask } from '#safe-action/actions'
const tasks = ref([{ id: '1', title: 'Try server actions', done: false }])
const { execute, optimisticData, isExecuting, serverError, cancel } = useOptimisticAction(toggleTask, {
currentData: tasks,
updateFn(input, current) {
return current.map(task => task.id === input.id ? { ...task, done: input.done } : task)
},
onSuccess({ data }) {
tasks.value = tasks.value.map(task => task.id === data.id ? { ...task, done: data.done } : task)
},
})
</script>
<template>
<ul>
<li v-for="task in optimisticData" :key="task.id">
<button @click="execute({ id: task.id, done: !task.done })">
{{ task.done ? '✓' : '○' }} {{ task.title }}
</button>
</li>
</ul>
<p v-if="isExecuting">Saving…</p>
<p v-if="serverError" role="alert">{{ serverError }}</p>
<button type="button" @click="cancel()">Cancel pending update</button>
</template>
Use this demonstration action for that example. It validates and echoes a desired state; connect real storage and authorization in your application.
import { z } from 'zod'
import { createSafeActionClient } from '#safe-action'
export default createSafeActionClient()
.schema(z.object({ id: z.string().min(1), done: z.boolean() }))
.outputSchema(z.object({ id: z.string(), done: z.boolean() }))
.action(({ parsedInput }) => parsedInput)
Ownership and reconciliation
Only one optimistic operation is owned at a time. Starting a newer execution cancels the earlier request, restores its baseline when still owned, and applies the new update to current authoritative data. This is replacement, not a queue of accumulating changes.
Failure/cancel/reset rolls back only if currentData.value is still the same value the helper installed. Replacing the ref during an external refresh prevents an older rollback from undoing that refresh. This identity guard does not detect in-place external edits; replace values and keep updateFn immutable.
On success, the optimistic value remains until your application reconciles it. The helper never assigns arbitrary action output to currentData; output may be a different shape. Use onSuccess or await executeAsync to merge a response or refresh a Nuxt query. Async callbacks are not awaited. Server mutations may still complete after client cancellation; refresh authoritative data where that distinction matters.
All useAction options apply. cancel() and reset() also release the helper's owned optimistic change. API reference.
If updateFn throws, no request starts; executeAsync rejects and execute logs the failure. Keep updateFn pure and immutable.