Fetching, Caching und Error Handling richtig gemacht
Daten zu laden klingt trivial – bis die erste API-Anfrage in der Produktion hängt, der Loading-State unklar ist oder veraltete Cache-Daten dem Nutzer ein falsches Bild der Realität zeigen. Vue 3 und Nuxt bieten ausgereifte Primitiven für Data Fetching, die zusammen Caching, Error Handling und SSR-Kompatibilität abdecken.
Inhaltsverzeichnis
- 1. Warum Data Fetching in Vue mehr ist als ein fetch()-Aufruf
- 2. Das useData-Composable: Fetching mit Loading und Error State
- 3. useFetch und useAsyncData in Nuxt 3
- 4. Caching-Strategien: vom einfachen Memo bis SWR
- 5. Error Handling: Netzwerkfehler, API-Fehler und Timeouts
- 6. Loading States und Skeleton-UX richtig gestalten
- 7. Paginierung und unendliches Scrollen
- 8. TanStack Query (Vue Query) für komplexe Anwendungsfälle
- 9. Fetching-Ansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Data Fetching in Vue mehr ist als ein fetch()-Aufruf
Wer zum ersten Mal Daten lädt in Vue, schreibt in onMounted einen fetch()-Aufruf und speichert das Ergebnis in einem ref. Das funktioniert für simple Prototypen, wird aber schnell unvollständig: Fehlerbehandlung fehlt, der Loading-State ist nicht definiert, beim erneuten Navigieren zur Route werden die Daten unnötig neu geladen, und bei Nuxt-SSR werden die Daten auf dem Client doppelt abgefragt. Jede dieser Lücken ist eine potenzielle Fehlerquelle im Produktionsbetrieb.
Das Grundproblem besteht darin, dass Daten laden in Vue ein asynchroner Vorgang mit mehreren Zuständen ist: Initialer Zustand (noch kein Request), Loading (Request läuft), Success (Daten verfügbar), Error (Request fehlgeschlagen), Revalidating (Daten werden im Hintergrund aktualisiert). Ein vollständiges Fetching-System modelliert alle diese Zustände explizit und macht sie für das Template reaktiv verfügbar. Die Composition API von Vue 3 ist dafür das ideale Werkzeug, weil Composables diese Zustandsmaschine sauber kapseln und wiederverwendbar machen.
2. Das useData-Composable: Fetching mit Loading und Error State
Das grundlegende Pattern für Daten laden in Vue ist ein useData-Composable, das den asynchronen Zustand in reaktive Refs übersetzt. Es nimmt eine Fetch-Funktion entgegen, ruft sie auf, und stellt data, isLoading, error und refresh nach außen zur Verfügung. Diese vier Werte sind alles, was eine Komponente für ein vollständiges UI benötigt: Skeleton während Loading, Fehlermeldung bei Error, Daten bei Success, und eine Möglichkeit, manuell zu aktualisieren.
Ein wichtiges Detail: Die Fetch-Funktion sollte als Parameter übergeben werden, nicht als URL-String. Das macht das Composable universell — es ist egal, ob intern fetch(), axios oder ein typisierter API-Client verwendet wird. Außerdem können Abhängigkeiten als reaktive Refs übergeben werden: Wenn sich ein Filter-Parameter ändert, löst das automatisch einen neuen Request aus. Dieses Muster verhindert den häufigen Bug, bei dem Filteränderungen die Datenliste nicht aktualisieren, weil kein Watcher auf die Abhängigkeit gesetzt wurde.
// composables/useData.ts — universal data fetching composable
import { ref, watch, type Ref } from 'vue'
interface UseDataOptions<T> {
immediate?: boolean // fetch on composable creation (default: true)
initialData?: T // data to show before first fetch completes
}
interface UseDataReturn<T> {
data: Ref<T | null>
isLoading: Ref<boolean>
error: Ref<string | null>
refresh: () => Promise<void>
}
export function useData<T>(
fetchFn: () => Promise<T>,
options: UseDataOptions<T> = {}
): UseDataReturn<T> {
const { immediate = true, initialData = null } = options
const data = ref<T | null>(initialData as T | null)
const isLoading = ref(false)
const error = ref<string | null>(null)
async function refresh() {
isLoading.value = true
error.value = null
try {
data.value = await fetchFn()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unbekannter Fehler'
} finally {
isLoading.value = false
}
}
if (immediate) refresh()
return { data, isLoading, error, refresh }
}
// Usage with reactive filter dependency
// const filter = ref({ category: 'shoes' })
// const { data, isLoading, error } = useData(() => fetchProducts(filter.value))
// watch(filter, () => refresh(), { deep: true })
3. useFetch und useAsyncData in Nuxt 3
Nuxt 3 bringt zwei spezialisierte Composables für das Daten laden in Vue-Kontext mit SSR: useFetch und useAsyncData. useFetch ist der einfachere Einstieg: Es nimmt eine URL, führt den Request sowohl auf dem Server als auch auf dem Client durch, und verhindert durch Payload-Hydration, dass der Client denselben Request nochmals stellt. useAsyncData ist die flexiblere Variante: Sie nimmt eine beliebige asynchrone Funktion und einen eindeutigen Key, über den Nuxt den Payload zur Hydration identifiziert. Beide Composables geben dieselbe reaktive API zurück: data, pending, error und refresh.
Ein entscheidender Unterschied zum reinen Vue-Composable: In Nuxt werden diese Composables während des Server-Rendering aufgerufen, bevor das HTML ausgeliefert wird. Das bedeutet, dass die Daten direkt im initialen HTML-Response enthalten sind — kein sichtbarer Loading-State für den Nutzer, kein Layout-Shift durch nachgeladene Inhalte, vollständig indexierbare Inhalte für Suchmaschinen. Das Daten laden in Vue mit Nuxt ist damit eine grundlegend andere Erfahrung als rein clientseitiges Fetching.
4. Caching-Strategien: vom einfachen Memo bis SWR
Caching beim Daten laden in Vue hat mehrere Ebenen. Die einfachste ist das In-Memory-Memo innerhalb eines Composables: Die Daten werden nach dem ersten Laden in einer Map gespeichert und bei erneutem Aufruf mit demselben Key sofort zurückgegeben. Das ist sinnvoll für Daten, die sich selten ändern — Kategorielisten, Konfigurationswerte, Referenzdaten. Die Schwäche: Die Daten werden beim Neuladen der Seite verworfen und können veraltet sein, ohne dass der Nutzer es bemerkt.
Stale-While-Revalidate (SWR) ist das modernere Caching-Pattern für Daten laden in Vue: Beim Aufruf werden sofort die gecachten (möglicherweise veralteten) Daten gezeigt, während im Hintergrund eine neue Anfrage läuft. Wenn die neue Antwort eintrifft, aktualisieren sich die Daten reaktiv. Der Nutzer sieht niemals einen leeren Loading-State für bereits gecachte Inhalte — nur einen kurzen Moment mit alten Daten, der durch die Hintergrund-Aktualisierung minimiert wird. Nuxt implementiert dieses Pattern mit der getCachedData-Option von useFetch, VueQuery implementiert es als Standard-Caching-Verhalten.
// composables/useCachedData.ts — SWR-style caching for Vue 3
import { ref, type Ref } from 'vue'
// Module-level cache — survives component re-mounts within the same session
const cache = new Map<string, { data: unknown; timestamp: number }>()
interface CachedDataOptions {
ttl?: number // time-to-live in milliseconds (default: 60 seconds)
staleWhileRevalidate?: boolean
}
export function useCachedData<T>(
key: string,
fetchFn: () => Promise<T>,
options: CachedDataOptions = {}
) {
const { ttl = 60_000, staleWhileRevalidate = true } = options
const data = ref<T | null>(null) as Ref<T | null>
const isLoading = ref(false)
const isRevalidating = ref(false)
const error = ref<string | null>(null)
async function load() {
const cached = cache.get(key)
const now = Date.now()
const isFresh = cached && (now - cached.timestamp) < ttl
if (isFresh) {
// Cache is fresh — return immediately, no request needed
data.value = cached.data as T
return
}
if (cached && staleWhileRevalidate) {
// Show stale data immediately, revalidate in background
data.value = cached.data as T
isRevalidating.value = true
} else {
isLoading.value = true
}
try {
const result = await fetchFn()
data.value = result
cache.set(key, { data: result, timestamp: Date.now() })
} catch (e) {
if (!data.value) {
error.value = e instanceof Error ? e.message : 'Ladefehler'
}
} finally {
isLoading.value = false
isRevalidating.value = false
}
}
load()
return { data, isLoading, isRevalidating, error, refresh: load }
}
5. Error Handling: Netzwerkfehler, API-Fehler und Timeouts
Fehler beim Daten laden in Vue kommen in verschiedenen Formen: Ein Netzwerkfehler bedeutet, dass der Server gar nicht erreicht wurde. Ein 4xx-HTTP-Fehler bedeutet, dass der Request falsch war — falsche Parameter, fehlende Berechtigung, nicht gefundene Ressource. Ein 5xx-Fehler deutet auf einen Server-Problem hin. Ein Timeout entsteht, wenn der Server antwortet, aber zu langsam. Jeder dieser Fälle erfordert eine andere Reaktion: Netzwerkfehler rechtfertigen einen Retry-Versuch, 401-Fehler erfordern eine Weiterleitung zur Login-Seite, 404-Fehler sollten eine leere Ansicht zeigen, 5xx-Fehler eine technische Fehlermeldung mit Option zum erneuten Laden.
Vue 3 bietet keine eingebaute Error-Boundary-Komponente wie React, aber das Muster lässt sich mit onErrorCaptured nachbauen. Eine ErrorBoundary-Komponente fängt Fehler aus allen Kindkomponenten ab und zeigt eine Fallback-UI an, statt die gesamte Anwendung mit einem weißen Bildschirm zu beenden. Kombiniert mit v-if/v-else-Blöcken im Template, die zwischen Loading-State, Fehler-State und Erfolg unterscheiden, ergibt sich eine vollständige Fehlerbehandlung für alle Szenarien des Daten Ladens in Vue.
6. Loading States und Skeleton-UX richtig gestalten
Der Loading-State ist der häufig vernachlässigte Teil beim Daten laden in Vue. Ein Spinner in der Mitte des Bildschirms ist das Minimum — aber nicht das Optimum. Skeleton-Screens, die die ungefähre Struktur der geladenen Inhalte andeuten, reduzieren die wahrgenommene Ladezeit erheblich. Der Nutzer sieht sofort, wo welche Inhalte erscheinen werden, und die Seite wirkt aktiv statt leer und wartend. In Vue implementiert man Skeleton-Screens als separate Komponenten, die dieselbe Grundstruktur wie die eigentliche Inhaltskomponente haben, aber mit animierten Platzhalterflächen statt echter Daten.
Ein weiterer wichtiger Aspekt des Loading-States beim Daten laden in Vue ist die Unterscheidung zwischen initialem Laden und Revalidierung. Beim initialen Laden ist kein Inhalt vorhanden — hier ist der Skeleton-Screen sinnvoll. Beim Revalidieren im Hintergrund (SWR) sind bereits Inhalte sichtbar — ein subtiler Indikator wie ein kleiner Spinner oben rechts oder eine dünne Fortschrittsleiste ist besser als das vollständige Ersetzen durch einen Skeleton-Screen. Die Regel: Niemals veraltete Inhalte durch einen Skeleton ersetzen — das ist eine Verschlechterung der UX, weil der Nutzer die vorhandenen Informationen verliert.
7. Paginierung und unendliches Scrollen
Paginierung ist ein häufiger Anwendungsfall beim Daten laden in Vue, der die Caching-Strategie beeinflusst. Seitenbasierte Paginierung mit expliziten Vor/Zurück-Buttons lädt eine bestimmte Seite der Ergebnisse und ersetzt die aktuelle Datenliste vollständig. Infinite Scroll hingegen fügt neue Ergebnisse an die bestehende Liste an. Beide Muster haben unterschiedliche Implikationen: Seitenbasierte Paginierung ist einfacher zu cachen und rückwärtskompatibel mit Browser-Back-Navigation. Infinite Scroll erzeugt eine bessere mobile UX, ist aber schwieriger zu debuggen wenn der Nutzer tief in der Liste navigiert und die URL keine Position enthält.
In Vue implementiert man Infinite Scroll mit dem IntersectionObserver-API: Ein unsichtbares Sentinel-Element am Ende der Liste wird beobachtet. Wenn es in den Viewport eintritt, wird der nächste Request ausgelöst. Im Composable verwaltet man die Gesamtliste als ref-Array, an das neue Seiten angehängt werden, und ein hasMore-Flag, das anzeigt, ob weitere Seiten existieren. Der Daten-Lade-Composable in Vue gibt loadMore als Methode nach außen, die die Seitennummer intern verwaltet und nur aufgerufen werden kann, wenn kein Request bereits läuft und noch Daten verfügbar sind.
8. TanStack Query (Vue Query) für komplexe Anwendungsfälle
Für Anwendungen mit komplexen Fetching-Anforderungen ist TanStack Query (früher VueQuery) eine Bibliothek, die alle besprochenen Muster fertig implementiert. Der Kern ist useQuery: Es nimmt einen Query-Key (für Caching und Invalidierung), eine Fetch-Funktion und Optionen entgegen. Es liefert data, isLoading, isFetching, error und refetch. Stale-While-Revalidate ist Standard, Caching und Deduplication von identischen Requests laufen automatisch. Das bedeutet: Wenn zehn Komponenten denselben Query-Key benutzen, wird dennoch nur ein HTTP-Request gesendet.
Die Stärke von TanStack Query beim Daten laden in Vue liegt in der Query-Invalidierung: Mit queryClient.invalidateQueries(['products']) werden nach einer Mutation alle Queries mit dem Key products als veraltet markiert und im Hintergrund neu geladen. Das löst das klassische Problem, dass nach einem POST oder DELETE die angezeigte Liste nicht aktualisiert wird, weil der Refetch-Mechanismus fehlt. Für einfachere Anwendungen — eine Handvoll API-Endpoints ohne komplexe Interdependenzen — ist ein eigenes Composable ausreichend. Ab einer gewissen Komplexität amortisiert sich die Einarbeitungszeit in TanStack Query schnell.
9. Fetching-Ansätze im Vergleich
Die Wahl des Fetching-Ansatzes beim Daten laden in Vue hängt von der Anwendungskomplexität, dem SSR-Bedarf und der gewünschten Caching-Tiefe ab.
| Ansatz | Caching | SSR-Kompatibel | Empfohlen für |
|---|---|---|---|
| onMounted + fetch | Keines | Nein | Prototypen, einfache SPAs |
| useData Composable | Optional, manuell | Manuell möglich | SPAs bis mittlere Komplexität |
| Nuxt useFetch | Payload-Hydration | Ja, automatisch | Nuxt-Projekte, SSR |
| TanStack Query | SWR, Invalidierung | Mit Nuxt-Plugin | Komplexe Datenabhängigkeiten |
| Pinia + Actions | Im Store-State | Mit $patch-Hydration | Feature-übergreifende Daten |
Für neue Nuxt-Projekte ist useFetch/useAsyncData der klare Standard. Für reine Vue-3-SPAs ohne SSR-Anforderung ist ein eigenes useData-Composable für einfache Fälle und TanStack Query für komplexe Datenabhängigkeiten die beste Kombination. Pinia-Actions als primärer Fetching-Mechanismus zu verwenden ist nur dann sinnvoll, wenn die geladenen Daten tatsächlich Feature-übergreifend geteilt werden müssen.
Mironsoft
Vue 3 und Nuxt Entwicklung, Performance und Data-Layer Engineering
Daten laden in Vue ohne robuste Fehlerbehandlung und Caching?
Wir bauen vollständige Data-Layer-Lösungen in Vue 3 und Nuxt – von einfachen Composables bis zu TanStack Query mit SWR, vollständiger Fehlerbehandlung und SSR-Kompatibilität.
Data-Layer-Design
Fetching-Strategie, Caching und Error Handling für euer Vue-Projekt
Nuxt-Migration
Migration von clientseitigem Fetching zu Nuxt useFetch mit SSR
Performance-Audit
Analyse von Wasserfall-Requests, doppeltem Fetching und fehlendem Caching
10. Zusammenfassung
Professionelles Daten laden in Vue ist mehr als ein fetch()-Aufruf in onMounted. Es erfordert die explizite Modellierung aller Zustände: Loading, Success, Error und Revalidating. Das useData-Composable kapselt diese Zustandsmaschine und macht sie wiederverwendbar. In Nuxt-Projekten übernehmen useFetch und useAsyncData zusätzlich die SSR-Hydration und verhindern doppelte Requests. Caching mit Stale-While-Revalidate verbessert die wahrgenommene Performance, indem veraltete Daten sofort gezeigt und im Hintergrund aktualisiert werden.
Error Handling beim Daten laden in Vue unterscheidet zwischen Netzwerkfehlern (Retry), 4xx-Fehlern (Anwendungsreaktion) und 5xx-Fehlern (technische Meldung). Loading States mit Skeleton-Screens statt Spinner verbessern die UX messbar. Für komplexe Anwendungen mit vielen Datenabhängigkeiten und Mutations lohnt sich TanStack Query, das alle besprochenen Muster fertig implementiert und durch Query-Invalidierung die Daten-Konsistenz nach Mutations sicherstellt.
Daten laden in Vue & Nuxt — Das Wichtigste auf einen Blick
Zustände modellieren
Immer alle vier Zustände explizit: Loading, Success, Error, Revalidating. Nie nur Daten speichern ohne Fehler- und Lade-State.
Nuxt-Fetching
useFetch für SSR mit automatischer Hydration. useAsyncData für benutzerdefinierte Fetch-Funktionen. Beide verhindern doppelte Requests.
Caching
Stale-While-Revalidate zeigt sofort gecachte Daten und aktualisiert im Hintergrund. Niemals Skeleton statt vorhandener Inhalte zeigen.
TanStack Query
Für komplexe Datenabhängigkeiten: automatisches SWR, Query-Deduplication und -Invalidierung nach Mutations. Amortisiert sich schnell.