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.
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,
useFetchwith a reactivequeryoption 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
- Access child component methods with template refs and defineExpose
- Add SEO metadata and structured data in Nuxt
- Build an accessible modal with Teleport
- Build custom inputs with defineModel and modifiers
- Catch component errors with an error boundary
- Debounce user input with a custom ref
- Fetch, refresh and share data with Nuxt useFetch
- Implement a dark mode toggle that respects system settings