Vize

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.

Open the interactive example

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
Request a delivery quote with cancellation: initial state
Initial state
Request a delivery quote with cancellation: state after the verified interactions
After the verified interactions

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.