Die Tücken des manuellen Datenabrufs mit useEffect und warum TanStack Query Ihre beste Wahl ist
Olivia Novak
Dev Intern · Leapcell

Einleitung
In der dynamischen Welt der Frontend-Entwicklung ist die Verwaltung asynchroner Operationen, insbesondere des Datenabrufs, ein Eckpfeiler für die Erstellung interaktiver und reaktionsfähiger Benutzeroberflächen. Der useEffect-Hook von React ist zwar leistungsstark für die Handhabung von Seiteneffekten, war aber oft die erste Wahl für den Datenabruf. Dieser gängige Ansatz kann jedoch, wenn er nicht sorgfältig gehandhabt wird, schnell zu einer Reihe von Problemen führen, darunter Race Conditions, übermäßige Re-Renders und komplexe Synchronisationslogik. Dieser Artikel befasst sich mit dem "useEffect-Datenabruf-Anti-Muster" – einem weit verbreiteten Problem, mit dem viele Entwickler konfrontiert sind – und argumentiert, warum die Nutzung moderner Datenabrufbibliotheken wie TanStack Query (früher React Query) eine weitaus elegantere, effizientere und wartungsfreundlichere Lösung bietet. Das Verständnis dieser Herausforderungen und die Übernahme besserer Muster sind keine reine Ästhetik; sie wirken sich direkt auf die Anwendungsleistung, die Entwicklererfahrung und die langfristige Wartbarkeit aus.
Das Problem des manuellen Datenabrufs über useEffect
Bevor wir uns mit dem "Warum" von TanStack Query befassen, sollten wir ein gemeinsames Verständnis der Kernkonzepte und damit verbundenen Probleme schaffen.
Kernterminologie
- Seiteneffekt (Side Effect): In React ist ein Seiteneffekt jede Operation, die etwas außerhalb des Geltungsbereichs der Komponente beeinflusst. Beispiele hierfür sind Datenabruf, manuelle Änderung des DOM, Abonnements und Timer.
useEffectist dafür konzipiert, diese zu handhaben. - Race Condition: Eine Race Condition tritt auf, wenn zwei oder mehr Operationen (z. B. Datenabrufe) gleichzeitig ausgeführt werden und ihr Ergebnis von der spezifischen Reihenfolge abhängt, in der sie abgeschlossen werden. Wenn die Reihenfolge nicht verwaltet wird, kann ein veralteter oder falscher Zustand angezeigt werden.
- Veraltete Daten (Stale Data): Daten, die nicht mehr aktuell oder korrekt sind, weil die Quelle aktualisiert wurde, die clientseitige Darstellung jedoch nicht.
- Cache-Invalidierung (Cache Invalidation): Der Prozess der Kennzeichnung von gecachten Daten als veraltet, was einen erneuten Abruf frischer Daten erzwingt.
- Query Key: In TanStack Query ein eindeutiger Bezeichner (typischerweise ein Array), der verwendet wird, um einen bestimmten Teil des Serverzustands im Cache eindeutig zu identifizieren.
Das useEffect-Datenabruf-Anti-Muster
Lassen Sie uns den typischen useEffect-Ansatz für den Datenabruf und seine inhärenten Probleme veranschaulichen.
Betrachten Sie eine einfache Komponente, die eine Liste von Beiträgen abruft:
import React, { useState, useEffect } from 'react'; function PostList() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchPosts = async () => { try { setLoading(true); const response = await fetch('https://api.example.com/posts'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); setPosts(data); } catch (e) { setError(e); } finally { setLoading(false); } }; fetchPosts(); }, []); // Leeres Abhängigkeitsarray bedeutet, dass dies einmal beim Mounten ausgeführt wird if (loading) return <div>Loading posts...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <h1>Posts</h1> <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); } export default PostList;
Das sieht unkompliziert aus, aber stellen Sie sich vor, wir müssen eine Suchleiste hinzufügen.
import React, { useState, useEffect } from 'react'; function SearchablePostList() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { // Füge einen Abort-Controller für Bereinigung und Race Conditions hinzu const abortController = new AbortController(); const signal = abortController.signal; const fetchPosts = async () => { try { setLoading(true); setError(null); // Vorherige Fehler löschen const url = `https://api.example.com/posts?q=${searchTerm}`; const response = await fetch(url, { signal }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); setPosts(data); } catch (e) { if (e.name === 'AbortError') { console.log('Fetch aborted'); } else { setError(e); } } finally { setLoading(false); } }; // Füge ein Debounce für eine bessere UX mit der Suchleiste hinzu const debounceTimeout = setTimeout(() => { fetchPosts(); }, 300); // Bereinigungsfunktion return () => { clearTimeout(debounceTimeout); abortController.abort(); // Laufende Fetches beim Unmounten oder Ändern der Abhängigkeiten abbrechen }; }, [searchTerm]); // Erneutes Ausführen, wenn sich searchTerm ändert if (loading) return <div>Loading posts...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <input type="text" placeholder="Search posts..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> <h1>Posts</h1> <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); }
Beachten Sie, wie schnell der Code unübersichtlich wird:
- Overhead für die Zustandsverwaltung: Wir verwalten manuell die Zustände
posts,loadingunderror. Für jeden Datenabruf wiederholt sich dieser Boilerplate-Code. - Race Conditions: Wenn
searchTermschnell geändert wird, können mehrere Fetch-Anfragen aktiv sein. Ohne ordnungsgemäße Bereinigung (wieAbortController) könnte eine ältere, langsamere Anfrage nach einer neueren, schnelleren aufgelöst werden, was zur Anzeige veralteter Daten führt. - Logik für erneuten Abruf: Was passiert, wenn der Benutzer wegnavigiert und dann zurückkehrt? Oder wenn die Daten auf dem Server veraltet sind? Wir haben keinen integrierten Mechanismus für automatische erneute Abrufe oder Hintergrundaktualisierungen.
- Caching: Es gibt keinen Caching-Mechanismus. Jedes Mal, wenn die Komponente gemountet wird oder sich Abhängigkeiten ändern, werden die Daten von Grund auf neu abgerufen.
- Anfragen-Deduplizierung (Deduping Requests): Wenn mehrere Komponenten versuchen, gleichzeitig dieselben Daten abzurufen, machen sie alle separate Anfragen.
- Fehlerbehandlung & Wiederholungsversuche: Es ist eine grundlegende Fehlerbehandlung vorhanden, aber fortgeschrittenere Funktionen wie automatische Wiederholungsversuche bei Fehlern fehlen.
- Synchronisation zwischen Komponenten: Das Teilen von Daten zwischen verschiedenen Komponenten, die dieselbe Ressource abrufen könnten, wird schwierig und erfordert Kontext oder globale Zustandsverwaltung.
Dieses "Anti-Muster" handelt nicht davon, dass useEffect an sich schlecht ist; es geht darum, dass useEffect nicht das richtige Werkzeug für die Verwaltung von Serverzuständen mit komplexer Lebenszyklusverwaltung und Optimierungen ist. Während useEffect einen Abruf initiieren kann, fehlen ihm die inhärenten Fähigkeiten, den gesamten Lebenszyklus von Serverzuständen zu verwalten.
TanStack Query tritt auf den Plan
TanStack Query (oft noch als React Query bezeichnet) ist eine leistungsstarke Bibliothek für die Verwaltung, das Caching und die Synchronisation von Serverzuständen in React-Anwendungen. Es verschiebt das Paradigma von der Behandlung von fetch als Seiteneffekt hin zur Behandlung von Daten als Serverzustand mit seinem eigenen Lebenszyklus und eigenen Belangen, die vom lokalen UI-Zustand getrennt sind.
Hier sehen Sie, wie die Komponente SearchablePostList mit TanStack Query aussehen würde:
import React, { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; // Ein einfacher künstlicher Debounce-Hook zur Veranschaulichung // In einer realen App könnten Sie eine Bibliothek wie "use-debounce" oder React's useDeferredValue verwenden function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } function SearchablePostListWithQuery() { const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm, 500); // Debounce Suchleiste const fetchPosts = async (queryKey) => { const [_key, { q }] = queryKey; // Destrukturieren des Query Keys const url = `https://api.example.com/posts?q=${q}`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }; const { data: posts, isLoading, isError, error, isFetching // Zeigt an, ob derzeit Daten abgerufen werden (nützlich für Hintergrundabrufe) } = useQuery({ queryKey: ['posts', { q: debouncedSearchTerm }], // Eindeutiger Schlüssel für diese Abfrage queryFn: fetchPosts, staleTime: 5 * 1000 * 60, // Daten gelten 5 Minuten als frisch keepPreviousData: true, // Vorherige Daten beim Abrufen neuer Daten anzeigen }); if (isError) return <div>Error: {error.message}</div>; return ( <div> <input type="text" placeholder="Search posts..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> {isLoading && debouncedSearchTerm === '' ? ( // Anfangsstatus ohne Suchbegriff <div>Loading posts...</div> ) : isFetching ? ( // Anderer Indikator beim erneuten Abrufen <div>Searching for "{debouncedSearchTerm}"...</div> ) : null} <h1>Posts</h1> <ul> {posts?.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); } export default SearchablePostListWithQuery;
Lassen Sie uns die Vorteile hier aufschlüsseln:
- Deklarativer Datenabruf: Wir deklarieren einfach
useQuerymit einemqueryKeyund einerqueryFn. TanStack Query kümmert sich um das "Wie". - Automatische Zustandsverwaltung:
isLoading,isError,datawerden alle vonuseQuerybereitgestellt. Kein manuellesuseStatemehr dafür. - Caching und Deduplizierung: TanStack Query cacht Daten automatisch basierend auf dem
queryKey. Wenn eine andere Komponente versucht,['posts', { q: 'react' }]abzurufen, während diese bereits im Cache sind (und nicht veraltet), erhält sie sofort die gecachten Daten ohne eine neue Netzwerkanfrage. Ausstehende Anfragen für denselben Schlüssel werden ebenfalls dedupliziert. - Stale-While-Revalidate: Standardmäßig verwendet TanStack Query eine "stale-while-revalidate"-Caching-Strategie. Es liefert sofort veraltete Daten aus, während es transparent frische Daten im Hintergrund abruft. Dies bietet eine hervorragende Benutzererfahrung. Die Option
staleTimeermöglicht es Ihnen zu konfigurieren, wie lange Daten als "frisch" gelten, bevor sie für Hintergrundabrufe berechtigt sind. - Verhinderung von Race Conditions: TanStack Query behandelt Race Conditions intern, indem es sicherstellt, dass nur das Ergebnis der letzten Abruffunktionsaufrufung auf den Zustand angewendet wird, wodurch frühere Promises, die außer Reihenfolge aufgelöst werden könnten, effektiv abgebrochen werden.
- Hintergrundabrufe: Daten werden automatisch erneut abgerufen, wenn:
- Der Benutzer die Internetverbindung wiederherstellt.
- Das Fenster neu fokussiert wird.
- Der Query Key geändert wird.
- Sie manuell einen erneuten Abruf auslösen.
- Fehlerbehandlung und Wiederholungsversuche: Eingebaute Wiederholungsmechanismen mit exponentieller Backoff bei Abfragefehlern.
keepPreviousData: Diese leistungsstarke Option stellt sicher, dass bei Änderung IhresqueryKey(z. B. Aktualisierung vonsearchTerm) die UI nicht plötzlich leer erscheint, während die neuen Daten geladen werden. Sie geht sanft über und zeigt die alten Daten an, bis die neuen Daten eintreffen.- Devtools: TanStack Query wird mit hervorragenden Devtools zur Inspektion von Cache, Abfragen und Mutationen geliefert.
Wann was verwenden?
Während TanStack Query ideal für Serverzustände ist, hat useEffect immer noch seinen Platz für andere Seiteneffekte:
useEffectfür UI-Seiteneffekte: DOM-Manipulation, Einrichten von Event-Listenern, Abonnieren externer Stores (wie ein globaler Theme-Kontext, bei demuseContextnicht ausreicht) oder Integration von Drittanbieterbibliotheken, die direkt auf das DOM wirken.useEffectfür die Synchronisation lokaler Zustände: Synchronisation des lokalen Komponentenzustands mit Props oder anderem lokalen Zustand (z. B. Zurücksetzen von Formularfeldern, wenn sich eine Item-ID-Prop ändert).
useEffect ist ein Low-Level-Primitiv zur Darstellung allgemeiner Seiteneffekte. TanStack Query ist eine High-Level-Abstraktion, die speziell für die Behandlung der Komplexität asynchroner Serverzustände entwickelt wurde.
Fazit
Das useEffect-Datenabruf-Anti-Muster entsteht durch die Verwendung eines generischen Seiteneffekt-Hooks zum Lösen eines spezialisierten Problems der Serverzustandsverwaltung. Obwohl es für einfache Fälle funktionieren kann, führt es zu erheblichem Boilerplate-Code und Komplexität, wenn es um Caching, Synchronisation und Race Conditions geht. TanStack Query befreit Entwickler von diesen Sorgen, indem es eine robuste, meinungsstarke und hoch optimierte Lösung für den deklarativen Umgang mit Serverdaten bietet. Durch die Einführung von Bibliotheken wie TanStack Query machen wir unsere Anwendungen leistungsfähiger, widerstandsfähiger und wartungsfreundlicher und bieten eine überlegene Entwickler- und Benutzererfahrung. Es ist an der Zeit, unsere Komponenten auf die UI zu konzentrieren und dedizierten Werkzeugen die Datenverwaltung zu überlassen.