use(): Der Hook, der die Regeln bricht (mit Absicht)
Reacts use()-Hook liest Promises und Context zur Renderzeit, integriert sich mit Suspense und beseitigt das häufigste useEffect-Anti-Pattern. Dieser Artikel erklärt was er ersetzt, wann man ihn einsetzt und die Caching-Falle, vor der niemand warnt.
Drei State-Variablen. Ein Cleanup-Flag. Ein Dependency-Array. Eine Race Condition, über die man jedes einzelne Mal nachdenken muss. Und das ist die korrekte Version: die meisten Codebasen überspringen das cancelled-Flag und das Error-Handling komplett.
Dieses Pattern ist nicht falsch. Es funktioniert. Aber es ist Boilerplate, das existiert, weil React keine eingebaute Möglichkeit hatte zu sagen: „Warte auf dieses Promise, dann rendere." Jede Komponente, die Daten abruft, musste dieselbe Loading/Error/Data-Zustandsmaschine von Grund auf neu erfinden.
React 19 hat use() eingeführt, um das zu lösen. Es ist der erste Hook, der in Bedingungen und Schleifen aufgerufen werden kann, er integriert sich direkt mit Suspense, und er verwandelt das Fetch-dann-setState-Pattern in eine einzige Zeile.
Was use() macht
use() liest einen Wert aus einer Ressource zur Renderzeit. Die Ressource kann ein Promise oder ein Context sein.
tsx
import{ use }from"react";
// Promise lesen - suspendiert bis aufgeloest
const user =use(userPromise);
// Context lesen - wie useContext, aber in Bedingungen aufrufbar
const theme =use(ThemeContext);
Das ist die gesamte API. Eine Funktion, zwei Anwendungsfälle.
Wenn du ein Promise übergibst, integriert sich use() mit der nächsten <Suspense>-Boundary. Solange das Promise aussteht, suspendiert die Komponente. React zeigt den Suspense-Fallback. Wenn es sich auflöst, rendert React mit dem aufgelösten Wert neu. Wenn es fehlschlägt, fängt die nächste Error Boundary den Fehler.
Kein useState. Kein useEffect. Kein isLoading. Kein setData. React übernimmt alles.
Das Pattern, das es ersetzt
Packe das Snippet von oben in eine Komponente und füge die obligatorischen Loading/Error-Guards hinzu:
Drei State-Deklarationen, ein Effect, drei bedingte Returns: alles bevor man die eigentliche UI erreicht. Jede Komponente, die Daten abruft, wiederholt diese Struktur.
Der Ladezustand wird von <Suspense> behandelt. Der Fehlerzustand wird von <ErrorBoundary> behandelt (aus dem react-error-boundary-Paket). Die Komponente selbst enthält nur den Happy Path - den Code, der läuft, wenn Daten verfügbar sind. Die Zustandsmaschine wurde von deinem Code in Reacts Runtime verlagert.
Da UserPage eine Server-Komponente ist, rendert sie nicht neu. Die Promise-Referenz wird einmal erstellt und als stabiler Prop weitergereicht, keine Caching-Akrobatik nötig.
Separation of Concerns
Beachte, wie die Komponente, die die Daten verwendet (UserProfile), von
der Komponente getrennt ist, die den Fetch initiiert und die
Loading/Error-UI definiert (UserPage). Das ist beabsichtigt. Der Consumer
weiß nicht, woher das Promise kommt oder was während des Wartens angezeigt
wird.
Wie Suspense reinpasst
use() funktioniert nicht isoliert. Es ist ein Teil einer dreiteiligen Architektur:
use(promise): suspendiert die Komponente, solange das Promise aussteht.
<Suspense fallback={...}>: fängt die Suspension ab und zeigt eine Fallback-UI.
<ErrorBoundary fallback={...}>: fängt abgelehnte Promises und zeigt eine Fehler-UI.
Ohne eine Suspense-Boundary darüber wird eine Komponente, die use() mit einem ausstehenden Promise aufruft, abstürzen. Die Boundary ist nicht optional.
tsx
<ErrorBoundaryfallback={<p>Etwas ist schiefgelaufen.</p>}>
<Suspensefallback={<p>Laden...</p>}>
<UserProfileuserPromise={userPromise}/>
</Suspense>
</ErrorBoundary>
Verschachtelte Boundaries
Du kannst Suspense-Boundaries verschachteln, um gestaffelte Ladesequenzen zu erstellen:
tsx
<Suspensefallback={<PageSkeleton/>}>
<HeaderuserPromise={userPromise}/>
<Suspensefallback={<FeedSkeleton/>}>
<FeedpostsPromise={postsPromise}/>
</Suspense>
</Suspense>
Wenn Header vor Feed auflöst, erscheint der Header sofort, während der Feed noch sein Skeleton zeigt. Jede Boundary steuert eine andere Ladezone. Das ist deklarative Lade-Orchestrierung, du beschreibst die Struktur, nicht das Timing.
Inhalte gemeinsam aufdecken
Alle Kinder innerhalb einer einzelnen Suspense-Boundary werden als Einheit behandelt. Wenn ein Kind suspendiert, zeigt die gesamte Boundary ihren Fallback. Das ist nützlich, wenn mehrere Daten gleichzeitig erscheinen sollen:
Alle drei Komponenten „poppen" zusammen rein, sobald jedes Promise aufgelöst ist. Keine Teilzustände, kein Layout-Shift.
Das Caching-Problem
Hier hören die meisten Tutorials auf. Aber wenn du versuchst, use() mit einem Promise zu verwenden, das innerhalb einer Client-Komponente erstellt wird, wirst du auf einen subtilen und frustrierenden Bug stoßen.
tsx
// Bug: Erstellt ein neues Promise bei jedem Render
functionUserProfile({ userId }:{ userId:string}){
const user =use(fetchUser(userId));// neues Promise bei jedem Render
return<ProfileCarduser={user}/>;
}
fetchUser(userId) gibt bei jedem Render ein neues Promise-Objekt zurück. React sieht ein neues Promise, suspendiert erneut, die Komponente rendert neu, erstellt ein weiteres neues Promise, suspendiert erneut. Endlosschleife.
Die goldene Regel
use() ruft keine Daten ab. Es liest ein Promise. Das Promise muss eine
stabile Identität über Renders hinweg haben. Wenn du bei jedem Render ein
neues Promise erstellst, bekommst du eine endlose Suspensions-Schleife.
Wie man das Promise stabilisiert
Es gibt mehrere Ansätze, jeder passend für eine andere Architektur:
1. Das Promise im Parent oder in einer Server-Komponente erstellen
Das ist das empfohlene Pattern. Der Parent erstellt das Promise einmalig und reicht es als Prop weiter:
tsx
// Server Component - Promise einmal erstellt, stabil über Renders
Kein async/await nötig, das Promise wird unaufgelöst weitergereicht. Die Client-Komponente entpackt es mit use(). Server-Komponenten rendern nicht neu, daher ist die Promise-Referenz von Natur aus stabil.
2. Einen Cache auf Modul-Ebene verwenden
Für Client-Komponenten, die Fetches initiieren müssen, cache das Promise, damit bei weiteren Aufrufen dieselbe Referenz zurückgegeben wird:
tsx
const cache =newMap<string,Promise<User>>();
functionfetchUserCached(id:string):Promise<User>{
if(!cache.has(id)){
cache.set(id,fetchUser(id));
}
return cache.get(id)!;
}
functionUserProfile({ userId }:{ userId:string}){
const user =use(fetchUserCached(userId));
return<ProfileCarduser={user}/>;
}
Gleiche Argumente ergeben dieselbe Promise-Referenz. Keine Endlosschleife.
Vermeide async in Cache-Wrappern
Markiere deine Cache-Funktion nicht als async. Das async-Schlüsselwort
erstellt immer ein neues Promise, selbst wenn du einen gecachten Wert
zurückgibst. Verwende eine synchrone Funktion, die das Original-Promise-Objekt
speichert und zurückgibt.
3. Eine Data-Fetching-Bibliothek verwenden
Bibliotheken wie TanStack Query oder SWR übernehmen Caching, Deduplizierung und Revalidierung von Haus aus. Sie existierten vor use() und lösen ein viel breiteres Problem - bringen aber auch ~13kB gzipped und einen Provider-Wrapper mit. Für ein einfaches "einmal fetchen, Ergebnis anzeigen"-Pattern reicht use() mit einer 5-Zeilen Cache-Funktion (Option 2 oben) völlig aus. Die Library lohnt sich, wenn die UI langlebigen Client-State hat, der aktuell bleiben muss: etwa Dashboards die bei Tab-Fokus refetchen, Listen mit Pagination oder Mutationen die verwandte Queries optimistisch updaten sollen.
4. Reacts cache() in Server-Komponenten verwenden
React bietet eine eingebaute cache()-Funktion für Server-Komponenten. Sie memoized den Rückgabewert einer Funktion für die Dauer eines einzelnen Server-Requests:
Mehrere Komponenten, die getUser("123") während desselben Server-Renders aufrufen, teilen sich einen Fetch. Der Cache ist auf den Request beschränkt, er wird bei jedem neuen Seitenaufruf zurückgesetzt.
cache() vs. useMemo
Beide memoizen. Aber cache() funktioniert komponentenübergreifend in einem
Server-Render (Deduplizierung), während useMemo innerhalb einer einzelnen
Komponente über Re-Renders hinweg funktioniert. cache() ist für Data
Fetching. useMemo ist für Berechnungen. Verschiedene Werkzeuge, verschiedene
Aufgaben.
use() für Context
use() kann auch Context lesen, und hier bricht es eine Regel, die jeder andere Hook befolgt.
Jeder React-Hook muss auf der obersten Ebene einer Komponente aufgerufen werden, nie in Bedingungen, Schleifen oder nach frühen Returns. use() ist die Ausnahme. Es kann bedingt aufgerufen werden:
Mit useContext würde dieser Code die Regeln der Hooks verletzen. Mit use() ist er gültig. Reacts Linter kennt diese Ausnahme.
Das ist relevant für Performance. Wenn eine Komponente Context nur in bestimmten Code-Pfaden braucht, lässt use() dich das Lesen komplett überspringen, wenn die Bedingung falsch ist. Mit useContext abonniert die Komponente diesen Context bedingungslos, selbst wenn sie den Wert nicht braucht.
Wann was verwenden
Die Landschaft des Data Fetchings in React hat mehr Optionen als je zuvor. Hier ist, wann welcher Ansatz der richtige ist:
Szenario
Ansatz
Server-Komponente ruft Daten ab
async/await direkt in der Komponente
Async-Daten von Server- an Client-Komponente übergeben
Promise auf dem Server erstellen, use() auf dem Client mit Suspense
Berechneter/abgeleiteter State aus Props oder anderem State
useMemo oder direkte Berechnung während des Renderings
Context bedingt lesen
use(SomeContext)
Context bedingungslos lesen
useContext(SomeContext): einfacher, vertrauter
Externes Store abonnieren (Redux, Zustand, Browser-API)
Der Lackmustest für useEffect
Wenn dein useEffect Daten abruft und setState mit dem Ergebnis aufruft,
macht es fast sicher Arbeit, die in eine Server-Komponente, eine
Daten-Bibliothek oder use() + Suspense gehört. Wenn dein useEffect einen
Event-Listener hinzufügt, einen Timer startet oder das DOM misst, das ist ein
tatsächlicher Seiteneffekt, und useEffect ist das richtige Werkzeug.
Migration weg von useEffect + setState
Wenn du eine bestehende Codebasis voller useEffect-Fetch-setState-Pattern hast, musst du nicht alles auf einmal umschreiben. Hier ist ein praktischer Migrationspfad:
Schritt 1: In einen Custom Hook extrahieren
Bevor du den Data-Fetching-Mechanismus änderst, kapsel das bestehende Pattern:
Das ändert den Mechanismus nicht, gibt dir aber eine einzige Stelle, an der du die Implementierung später austauschen kannst.
Schritt 2: Suspense-Boundaries hinzufügen
Umwickle die konsumierenden Komponenten mit Suspense und Error Boundaries. Das ist sicher, auch bevor du zu use() wechselst: die Boundaries triggern einfach noch nicht:
tsx
<ErrorBoundaryfallback={<ErrorMessage/>}>
<Suspensefallback={<Skeleton/>}>
<UserProfileuserId={userId}/>
</Suspense>
</ErrorBoundary>
Schritt 3: Die Interna austauschen
Ändere jetzt den Custom Hook (oder die Komponente), sodass er ein Promise akzeptiert und use() verwendet. Die konsumierenden Komponenten ändern sich nicht, sie haben bereits Suspense-Boundaries:
Der alte useUser-Hook kann gelöscht werden. Die drei State-Variablen, der Effect und das Cleanup-Flag sind weg. Die Boundary übernimmt Loading und Errors.
Schritt 4: Den Fetch nach oben verschieben
Verschiebe die Promise-Erstellung in Server-Komponenten oder in eine Caching-Schicht. Das ist der eigentliche architektonische Wandel, die Daten-Initiierung bewegt sich von der Komponente, die sie braucht, zur Komponente (oder zum Server), die eine stabile Referenz erstellen kann.
Regeln und Stolperfallen
use() kann bedingt aufgerufen werden
Anders als jeder andere Hook funktioniert use() in if, for und nach frühen Returns. Reacts Linter kennt diese Ausnahme.
use() kann nicht in try-catch aufgerufen werden
Abgelehnte Promises werden von Error Boundaries gefangen, nicht von try-catch-Blöcken. Wenn du use() in ein try-catch wickelst, wirft React einen „Suspense Exception"-Fehler.
tsx
// Das wird abstuerzen
try{
const data =use(promise);
}catch(e){
// Wird nie erreicht
}
Wenn du einen Fallback-Wert für ein abgelehntes Promise bereitstellen musst, verwende .catch() auf dem Promise selbst:
Aufgelöste Werte müssen serialisierbar sein (Server zu Client)
Wenn du ein Promise von einer Server-Komponente an eine Client-Komponente übergibst, muss der aufgelöste Wert serialisierbar sein, keine Funktionen, keine Klassen-Instanzen, keine Symbols. Primitive, einfache Objekte und Arrays sind in Ordnung.
Mische keine Patterns für dieselben Daten
Wenn du ein Promise mit use() liest, rufe nicht auch dieselben Daten in einem useEffect ab. Wähle eine Quelle der Wahrheit für jedes Datenstück.
Ein vollständiges Beispiel
Hier ist ein realistisches Beispiel, ein Dashboard, das ein Benutzerprofil und die letzten Bestellungen parallel lädt, mit gestaffeltem Loading:
Der folgende Ablauf zeigt die genaue Reihenfolge, was wann rendert und was der Benutzer in jeder Phase sieht:
DIAGRAM
Die zentrale Erkenntnis: use() gibt nie einen undefined oder ausstehenden Wert zurück. Wenn user.name ausgeführt wird, ist das Promise bereits aufgelöst. Solange es aussteht, rendert die Komponente einfach nicht, die nächste Suspense-Boundary zeigt stattdessen ihren Fallback.
Was ist mit Client-seitigem Fetching?
Alle bisherigen Beispiele starten den Fetch in einer Server Component und reichen das Promise nach unten. Aber was, wenn man bereits tief in einer Client Component steckt, zum Beispiel ein Button öffnet ein Popup, das neue Daten braucht?
Man kann keine Server Component innerhalb einer Client Component rendern. Aber use() + Suspense funktioniert trotzdem, man muss nur die Promise-Identität selbst verwalten.
Den Fetch im Event Handler starten
Der direkteste Ansatz: das Promise im Click-Handler erzeugen, im State speichern und use() lesen lassen.
Das ist reaktionsschnell, der Fetch startet in dem Moment, in dem der Benutzer klickt, nicht erst wenn React einen Render plant. Jeder Klick erzeugt ein neues Promise, sodass man immer aktuelle Daten bekommt.
Modul-Level Cache für wiederholten Zugriff
Wenn dasselbe Popup mehrfach mit derselben ID geöffnet werden könnte, vermeidet ein Cache redundante Requests:
Das ist exakt dasselbe Caching-Pattern wie im Abschnitt oben, es funktioniert
identisch, egal ob das Promise in einer Server Component oder einem
Click-Handler erzeugt wird. Entscheidend ist, dass use() immer dasselbe
Promise-Objekt für denselben Request erhält.
Wann reicht use(), und wann braucht man mehr?
Für das Popup-Szenario oben. Benutzer klickt, Daten laden, Popup zeigt sie an, reicht use() mit einer einfachen Cache-Funktion völlig aus. Keine Extra-Dependency, kein Provider, keine Konfiguration. Der 5-Zeilen Map-Cache von oben erledigt die Deduplizierung.
Ziehe TanStack Query oder SWR in Betracht, wenn die Daten einen Lebenszyklus über eine einzelne Anzeige hinaus haben:
Dieselben Daten werden an mehreren Stellen angezeigt und eine Mutation an einer Stelle soll alle anderen aktualisieren
Daten werden stale und sollen still refetchen, wenn der Benutzer zum Tab zurückkehrt
Du brauchst paginierte oder Infinite-Scroll-Listen mit Cursor-Tracking
Du willst optimistisches UI, das bei Server-Fehler zurückrollt
Wenn nichts davon zutrifft, ist use() + eine Cache-Funktion die einfachere Wahl. Man kann später jederzeit eine Library hinzufügen, wenn die Caching-Anforderungen wachsen, das mentale Modell (Promise rein, Daten raus, Suspense handhabt das Warten) bleibt dasselbe.