Drie staten — altijd
Elke async fetch heeft drie mogelijke staten. Toon ze allemaal goed, en je gebruiker weet altijd wat er aan de hand is:
- Loading — bezig met laden, toon een spinner of "Laden..."
- Error — er ging iets fout, toon de melding
- Success — de data is binnen, toon hem
Beginnersfout: alleen de data tonen, en bij een fetch een leeg scherm laten zien. De gebruiker denkt dan dat de app stuk is. Toon altijd de loading-staat.
Het patroon — drie refs
Maak voor elke staat een eigen ref:
<script setup>
import { ref, onMounted } from 'vue'
const data = ref(null)
const loading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const res = await fetch('https://api.example.com/data')
if (!res.ok) {
throw new Error(`Server gaf ${res.status}`)
}
data.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
</script>
Waarom finally? Wat er ook gebeurt — succes of error — de loader moet uit. finally draait áltijd, dus daar zet je loading.value = false.
Weergave in template
Met v-if / v-else-if / v-else kies je welke staat zichtbaar is:
<template>
<p v-if="loading">Laden...</p>
<p v-else-if="error" class="error">
Er ging iets fout: {{ error }}
</p>
<ul v-else>
<li v-for="user in data" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
Volgorde is belangrijk:
- Eerst
v-if="loading"— anders zie je even "geen data" voor de fetch start - Dan
v-else-if="error" - Pas in
v-elsede data — alleen als alles goed ging
Compleet voorbeeld
<script setup>
import { ref, onMounted } from 'vue'
const users = ref([])
const loading = ref(true)
const error = ref(null)
const loadUsers = async () => {
loading.value = true
error.value = null
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
users.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
onMounted(() => {
loadUsers()
})
</script>
<template>
<h1>Gebruikers</h1>
<div v-if="loading" class="loader">
⏳ Laden...
</div>
<div v-else-if="error" class="error-box">
<p>Er ging iets fout: {{ error }}</p>
<button @click="loadUsers">Opnieuw proberen</button>
</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }} — {{ user.email }}
</li>
</ul>
</template>
<style scoped>
.loader { padding: 24px; text-align: center; color: #666; }
.error-box {
padding: 16px;
background: #fee;
border: 1px solid #f99;
border-radius: 8px;
}
</style>
Bonus: de "Opnieuw proberen" knop roept gewoon loadUsers() nog een keer aan. Doordat de functie de loading/error refs reset, werkt de retry vanzelf.
Als composable — herbruikbaar
Gebruik je dit patroon in meerdere componenten? Trek het uit in een composable.
// src/composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(true)
const error = ref(null)
const execute = async () => {
loading.value = true
error.value = null
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
execute() // start meteen
return { data, loading, error, execute }
}
Gebruik in een component
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data: users, loading, error, execute } = useFetch(
'https://jsonplaceholder.typicode.com/users'
)
</script>
<template>
<p v-if="loading">Laden...</p>
<p v-else-if="error">Fout: {{ error }}</p>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="execute">Opnieuw laden</button>
</template>
Tip: VueUse heeft een kant-en-klare useFetch die dit (en meer) doet. Voor productie-apps gebruik je die in plaats van zelf bouwen.
Belangrijke Regels
- Altijd drie refs:
data,loading,error loadingbegint optrue— anders zie je heel even een leeg schermloading.value = falseinfinally— draait altijd- Volgorde in template: loading → error → data
- Vergeet de retry-knop niet bij errors — gebruikers willen het opnieuw proberen
Veelgemaakte Fouten
Fout — loading = false alleen bij success:
try {
data.value = await res.json()
loading.value = false // ❌ bij error blijft loading true!
} catch (e) {
error.value = e.message
}
Goed — in finally:
try {
data.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false // ✅ altijd
}
Fout — verkeerde volgorde in template:
<ul v-if="data">...</ul>
<p v-else-if="loading">Laden...</p>
<!-- ❌ tijdens loading is data nog null → toont eerst "Laden..." — werkt toevallig -->
<!-- maar logica is rommelig -->
Goed:
<p v-if="loading">Laden...</p>
<p v-else-if="error">{{ error }}</p>
<ul v-else>...</ul>