async-resource
Create a scoped, abortable asynchronous resource with latest-result-wins state.
| Package | @vizejs/composable/async-resource |
| Own the source | vize lib pull composable:async-resource |
| Runtime exports | useAsyncResource |
| Gzip budget | 2048 B |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/composable
Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.
import { useAsyncResource } from "@vizejs/composable/async-resource";
Try it
Request a delivery quote with cancellation
Let a shopper change delivery service or cancel a quote request while keeping the last successful price visible.
What to observe: Start Standard, then immediately Express. The slow Standard loader is aborted and only Express appears. Cancel request stops a pending producer without adding a response. Unavailable area shows a failure while retaining the last quote; choose a service to recover. Reset quote aborts pending work and clears the result.
This is delayed local sample data, not a server integration: Standard takes 800 ms and the other choices take 300 ms. The producer listens to the real AbortSignal, clears its timer on abort and removes its listener when it settles. In an application pass that signal to fetch. keepData and cancelPrevious default to true. execute() resolves to a success, error, cancelled or superseded result; a loader rejection does not reject that promise. The response/cancellation counters and last execution label are example-owned history, so reset does not clear them. Call inside setup: no loader runs during SSR, and disposing the component scope aborts active work through the same signal. The package remains experimental.
Compare the initial and interacted states


Copy the example
Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.
<script setup lang="ts">
import { ref } from "vue";
import { useAsyncResource } from "@vizejs/composable/async-resource";
type Service = "standard" | "express" | "unavailable";
interface Quote {
service: string;
price: string;
}
const responses = ref(0);
const aborted = ref(0);
const lastExecution = ref("none");
const { data, error, status, pending, execute, cancel, reset } = useAsyncResource(
({ signal }, service: Service) =>
new Promise<Quote>((resolve, reject) => {
// Local sample data: replace this producer with fetch(url, { signal }) in an app.
const onAbort = () => {
clearTimeout(timer);
aborted.value += 1;
reject(signal.reason);
};
const timer = setTimeout(
() => {
signal.removeEventListener("abort", onAbort);
responses.value += 1;
if (service === "unavailable") reject(new Error("No courier serves this sample area."));
else
resolve({
service: service === "standard" ? "Standard delivery" : "Express delivery",
price: service === "standard" ? "€4.90" : "€9.90",
});
},
service === "standard" ? 800 : 300,
);
if (signal.aborted) onAbort();
else signal.addEventListener("abort", onAbort, { once: true });
}),
);
async function request(service: Service): Promise<void> {
// execute() resolves to success/error/cancelled/superseded; loader failures do not reject it.
const result = await execute(service);
lastExecution.value = result.status;
}
</script>
<template>
<div class="composable-example">
<p>Try delayed local delivery quotes. No request is sent to a server.</p>
<div class="example-actions">
<button type="button" @click="() => request('standard')">Standard</button>
<button type="button" @click="() => request('express')">Express</button>
<button type="button" @click="() => request('unavailable')">Unavailable area</button>
<button type="button" :disabled="!pending" @click="() => cancel()">Cancel request</button>
<button type="button" @click="() => reset()">Reset quote</button>
</div>
<output aria-live="polite">{{
status === "pending"
? "Getting a quote…"
: status === "cancelled"
? "Quote request cancelled."
: status === "error"
? "Could not get a quote."
: status === "success"
? "Quote ready."
: "Choose a delivery service."
}}</output>
<p v-if="error instanceof Error" role="alert">{{ error.message }}</p>
<section v-if="data" class="quote" aria-label="Current quote">
<strong>{{ data.service }} · {{ data.price }}</strong>
<p>The last successful quote stays visible until reset.</p>
</section>
<p v-else>No delivery quote yet.</p>
<p>
{{ responses }} {{ responses === 1 ? "response" : "responses" }} · {{ aborted }} cancelled
{{ aborted === 1 ? "request" : "requests" }}
</p>
<p>Last execution result: {{ lastExecution }}</p>
</div>
</template>
<style scoped>
.quote {
border: 1px solid #888;
border-radius: 6px;
padding: 12px;
& p {
margin-bottom: 0;
}
}
</style>
Runtime contract
| Utility | Category | Stability | SSR | Hydration | Cleanup | Targets | Host globals | Uses |
|---|---|---|---|---|---|---|---|---|
useAsyncResource |
async | experimental | safe | stable | caller, reactive-scope | web, server, worker, native, desktop, terminal | AbortController, DOMException |
tryOnScopeDispose |
API
useAsyncResource
Create a scoped, abortable asynchronous resource with latest-result-wins state. Every execution returns a discriminated result, so cancellation, supersession, loader failure, and successful undefined data stay distinct. When created inside an active reactive scope (and scope is enabled), the active execution is aborted when that scope stops; outside a scope, cancellation ownership stays with the caller. The execute promise never rejects — synchronous and asynchronous loader failures both settle into the "error" result. Safe during server rendering: no browser globals are read and abort reasons use the runtime-native DOMException.
function useAsyncResource<Data, Arguments extends readonly unknown[], Failure = unknown>( loader: (context: AsyncResourceContext, ...arguments_: Arguments) => Promise<Data>, options: UseAsyncResourceOptions<Data> = {}, ): AsyncResource<Data, Arguments, Failure>
Types
AsyncResourceContext
Context supplied to an asynchronous resource loader.
| Member | Type | Description |
|---|---|---|
signal |
AbortSignal |
Signal aborted by cancellation, reset, scope disposal, or a newer execution. |
UseAsyncResourceOptions
Options for useAsyncResource.
| Member | Type | Description |
|---|---|---|
initialData? |
Data |
Initial data restored by AsyncResource.reset. |
cancelPrevious? |
boolean |
Abort the active execution when a newer execution starts. |
keepData? |
boolean |
Retain the current data while a new execution is pending. |
scope? |
boolean |
Cancel an active execution when the current reactive scope is disposed. |
AsyncResource
Reactive state and controls for an asynchronous loader.
| Member | Type | Description |
|---|---|---|
data |
Readonly<ShallowRef<Data | undefined>> |
Data of the newest successful execution, retained according to keepData. |
error |
Readonly<ShallowRef<Failure | undefined>> |
Failure of the newest settled execution, cleared when a new one starts. |
status |
Readonly<Ref<AsyncResourceStatus>> |
Current lifecycle status, driven only by the newest execution. |
pending |
ComputedRef<boolean> |
Whether an execution is currently pending. |
execute |
(...arguments_: Arguments) => Promise<AsyncResourceExecution<Data, Failure>> |
Run the loader. The returned promise never rejects: loader failures, cancellation, and supersession are reported as the discriminated result, and stale executions leave the reactive state untouched. |
cancel |
(reason?: unknown) => boolean |
Abort the active execution and mark the resource cancelled. |
reset |
() => void |
Cancel any active execution and restore the initial idle state. |