Wrap Fetch in a Reusable Hook
useFetch Composable
Build a typed useFetch that returns reactive data/error/loading and refetches when its url changes — or use VueUse's battle-tested version.
What you'll learn
- Implement a generic useFetch
- Auto-refetch when the url ref changes
- Know when to reach for VueUse instead
Inlining fetch logic in every component gets old. A useFetch composable centralizes loading/error handling, supports a reactive URL, and is easy to test.
A Minimal useFetch
// composables/useFetch.ts
import { ref, watch, toValue, type MaybeRefOrGetter } from 'vue'
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null)
const error = ref<unknown>(null)
const loading = ref(false)
watch(
() => toValue(url),
async (u) => {
loading.value = true
error.value = null
try {
const res = await fetch(u)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = (await res.json()) as T
} catch (e) {
error.value = e
} finally {
loading.value = false
}
},
{ immediate: true },
)
return { data, error, loading }
} MaybeRefOrGetter<string> accepts plain strings, refs, and getters — flexible to call.
Using It
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useFetch } from '@/composables/useFetch'
interface User { id: number; name: string }
const route = useRoute()
const { data, error, loading } = useFetch<User>(() => `/api/users/${route.params.id}`)
</script>
<template>
<p v-if="loading">Loading…</p>
<p v-else-if="error">Failed.</p>
<p v-else>Hello {{ data?.name }}</p>
</template> When the route param changes, the URL getter returns a new value, the watcher fires, and the request refetches.
VueUse
VueUse ships a battle-tested useFetch with abort/cancel, refetching, and shorthand methods (get, post). For real apps, prefer it over rolling your own.
npm i @vueuse/core import { useFetch } from '@vueuse/core'
const { data, error, isFetching } = useFetch('/api/users').json()