Vue cookbook · Intermediate

Fetch data in a composable and cancel stale requests

Typing in a search box fires overlapping requests, and slow old responses overwrite newer results.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Watch the reactive input, create an AbortController per request, and abort the previous one with onWatcherCleanup (Vue 3.5). Track loading and error state alongside the data.

Solution

// composables/useProducts.ts
import { ref, watch, onWatcherCleanup, toValue, type MaybeRefOrGetter } from "vue";

type Product = { id: number; name: string };

export function useProducts(query: MaybeRefOrGetter<string>) {
  const data = ref<Product[]>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);

  watch(() => toValue(query), async (q) => {
    const controller = new AbortController();
    onWatcherCleanup(() => controller.abort());          // runs when the query changes again
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(`/api/products?q=${encodeURIComponent(q)}`, { signal: controller.signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      data.value = await res.json();
    } catch (e) {
      if ((e as Error).name !== "AbortError") error.value = (e as Error).message;
    } finally {
      if (!controller.signal.aborted) loading.value = false;
    }
  }, { immediate: true });

  return { data, loading, error };
}
<script setup lang="ts">
import { ref } from "vue";
import { useProducts } from "./composables/useProducts";

const query = ref("");
const { data: products, loading, error } = useProducts(query);
</script>

<template>
  <input v-model="query" placeholder="Search products" />
  <p v-if="loading">Loading...</p>
  <p v-else-if="error" role="alert">{{ error }}</p>
  <ul v-else>
    <li v-for="p in products" :key="p.id">{{ p.name }}</li>
  </ul>
</template>

Pitfalls

  • Not cancelling previous requests, so responses arrive out of order.
  • Showing aborted requests as errors.
  • Fetching on every keystroke without debouncing (see the debounced input recipe).

Variations

  • In Nuxt, useFetch with a reactive query option handles keys, SSR and cancellation for you.
  • TanStack Query (Vue Query) adds caching, retries and background refetching.

Further reading: onWatcherCleanup

Reference: https://vuejs.org/api/reactivity-core.html#onwatchercleanup

More Vue recipes

Was this page helpful?