useFetch Composable

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.

5 min read Level 3/5 #vue#composables#fetch
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()
Suspense →