SkillByAIOpen interactive version →

Lesson 20 / 25

Fetching Data

Loading, error and data states.

Async composables and query libraries

Every request has at least three states: loading, error and success, and the UI should render all of them. A small composable can hold data, error and loading refs, re-fetch when a reactive input changes (using watchEffect with toValue() on a ref or getter argument), and ignore stale responses when inputs change quickly. For larger apps, TanStack Query for Vue (@tanstack/vue-query) adds caching keyed by a queryKey, deduplication, background refetching and retries through useQuery, plus useMutation for writes. Its option and result names have changed between major versions, so check the docs for your version.

A useFetch composable and its use

Handles all three states.

// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {          // url: string, ref or getter
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  watchEffect(async (onCleanup) => {
    let cancelled = false
    onCleanup(() => { cancelled = true })   // a newer request replaced this one
    loading.value = true
    error.value = null
    try {
      const res = await fetch(toValue(url))
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      const json = await res.json()
      if (!cancelled) data.value = json
    } catch (e) {
      if (!cancelled) error.value = e
    } finally {
      if (!cancelled) loading.value = false
    }
  })

  return { data, error, loading }
}

// <script setup>
// const props = defineProps({ id: String })
// const { data: user, error, loading } = useFetch(() => `/api/users/${props.id}`)
// </script>
// <template>
//   <p v-if="loading">Loading...</p>
//   <p v-else-if="error">Could not load user: {{ error.message }}</p>
//   <UserCard v-else-if="user" :user="user" />
// </template>

A restaurant order screen

A good order screen shows 'preparing', 'ready' or 'problem with your order', never a blank board. Your UI should be just as clear about loading, success and error.

Quick check: Why does the composable call toValue(url) inside watchEffect?

  • To convert the URL to lowercase
  • So the effect tracks a ref or getter argument and refetches when it changes
  • To cache the response forever
  • Because fetch only accepts refs
Answer

So the effect tracks a ref or getter argument and refetches when it changes — Reading the reactive source inside the effect registers it as a dependency.