Skip to content
← All questions
⚡ Intermediate

How would you build a composable for data fetching?

🪝 Composables🍍 Pinia🧰 VueUse👁️ Watchers

Data fetching is one of the first things you'll extract into a composable. Every component that loads data from an API repeats the same pattern: a loading flag, an error state, the actual data, and the fetch logic. A useFetch composable wraps all of that into a reusable function.

Basic implementation

ts
import { ref, toValue, watchEffect, type MaybeRefOrGetter } from 'vue'

export function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const data = ref<T | null>(null)
  const error = ref<string | null>(null)
  const loading = ref(false)

  async function execute() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(toValue(url))
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      data.value = await response.json()
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Unknown error'
    } finally {
      loading.value = false
    }
  }

  watchEffect(() => {
    execute()
  })

  return { data, error, loading, refetch: execute }
}

Key design decisions:

  • MaybeRefOrGetter<string>: the URL can be a plain string, a ref, or a getter. toValue() unwraps whatever it is. This is the Vue 3.3+ convention for composable inputs.
  • watchEffect: runs immediately (fetches on creation) and re-runs whenever the URL changes. If the URL is static, it fetches once.
  • Returns refs: the caller gets reactive data, error, and loading that update as the request progresses.
  • refetch: exposes the execute function so the caller can retry or refresh manually.

Using it in a component

vue
<script setup lang="ts">
import { computed } from 'vue'
import { useFetch } from '@/composables/useFetch'

const props = defineProps<{ userId: number }>()

const {
  data: user,
  error,
  loading
} = useFetch<User>(() => `/api/users/${props.userId}`)
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error }}</div>
  <div v-else-if="user">
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>
Open in Vue Playground

Passing a getter (() => /api/users/${props.userId}) means the composable refetches automatically when userId changes.

What about AbortController?

A production composable should cancel in-flight requests when the URL changes or the component unmounts. See How do you use AbortController in a composable? for that pattern.

When to use a library instead

For real apps, consider VueUse's useFetch or dedicated data-fetching libraries like Pinia Colada or TanStack Query. They handle caching, deduplication, retry logic, and stale-while-revalidate patterns that a simple composable doesn't cover.

See also: What is a composable? · How do you use AbortController in a composable? · What is VueUse?

References

Released under the MIT License.