4. Februar 2026
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.
Sascha Becker
Author17 Min. Lesezeit
use(): Der Hook, der die Regeln bricht (mit Absicht)
Jeder React-Entwickler hat diesen Code geschrieben:
tsxconst [data, setData] = useState<User | null>(null);const [isLoading, setIsLoading] = useState(true);const [error, setError] = useState<Error | null>(null);useEffect(() => {let cancelled = false;setIsLoading(true);fetchUser(id).then((user) => {if (!cancelled) setData(user);}).catch((err) => {if (!cancelled) setError(err);}).finally(() => {if (!cancelled) setIsLoading(false);});return () => {cancelled = true;};}, [id]);
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.
tsximport { use } from "react";// Promise lesen - suspendiert bis aufgeloestconst user = use(userPromise);// Context lesen - wie useContext, aber in Bedingungen aufrufbarconst 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:
tsxfunction UserProfile({ userId }: { userId: string }) {const [user, setUser] = useState<User | null>(null);const [isLoading, setIsLoading] = useState(true);const [error, setError] = useState<Error | null>(null);useEffect(() => {/* ... fetch, cancelled flag, setState ... */}, [userId]);if (isLoading) return <Skeleton />;if (error) return <ErrorMessage error={error} />;if (!user) return null;return <ProfileCard user={user} />;}
Drei State-Deklarationen, ein Effect, drei bedingte Returns: alles bevor man die eigentliche UI erreicht. Jede Komponente, die Daten abruft, wiederholt diese Struktur.
Dieselbe Komponente mit use():
tsx// Client Component - nur der Happy Path"use client";import { use } from "react";function UserProfile({ userPromise }: { userPromise: Promise<User> }) {const user = use(userPromise);return <ProfileCard user={user} />;}
tsx// Server Component - erstellt das Promise und definiert die Boundariesimport { Suspense } from "react";import { ErrorBoundary } from "react-error-boundary";export default function UserPage({ params }: { params: { id: string } }) {const userPromise = fetchUser(params.id);return (<ErrorBoundary fallback={<ErrorMessage />}><Suspense fallback={<Skeleton />}><UserProfile userPromise={userPromise} /></Suspense></ErrorBoundary>);}
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<ErrorBoundary fallback={<p>Etwas ist schiefgelaufen.</p>}><Suspense fallback={<p>Laden...</p>}><UserProfile userPromise={userPromise} /></Suspense></ErrorBoundary>
Verschachtelte Boundaries
Du kannst Suspense-Boundaries verschachteln, um gestaffelte Ladesequenzen zu erstellen:
tsx<Suspense fallback={<PageSkeleton />}><Header userPromise={userPromise} /><Suspense fallback={<FeedSkeleton />}><Feed postsPromise={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:
tsx<Suspense fallback={<DashboardSkeleton />}><Stats statsPromise={statsPromise} /><Chart chartPromise={chartPromise} /><RecentActivity activityPromise={activityPromise} /></Suspense>
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 Renderfunction UserProfile({ userId }: { userId: string }) {const user = use(fetchUser(userId)); // neues Promise bei jedem Renderreturn <ProfileCard user={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 Rendersexport default function UserPage({ params }: { params: { id: string } }) {const userPromise = fetchUser(params.id);return (<Suspense fallback={<Skeleton />}><UserProfile userPromise={userPromise} /></Suspense>);}
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:
tsxconst cache = new Map<string, Promise<User>>();function fetchUserCached(id: string): Promise<User> {if (!cache.has(id)) {cache.set(id, fetchUser(id));}return cache.get(id)!;}function UserProfile({ userId }: { userId: string }) {const user = use(fetchUserCached(userId));return <ProfileCard user={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:
tsximport { cache } from "react";const getUser = cache(async (id: string): Promise<User> => {const res = await fetch(`/api/users/${id}`);return res.json();});
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:
tsxfunction Greeting({ showFormal }: { showFormal: boolean }) {if (showFormal) {const { locale } = use(I18nContext);return <p>{locale === "de" ? "Guten Tag" : "Good day"}</p>;}return <p>Hey!</p>;}
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 |
| Einfacher Client-Fetch (Popup, Dialog, einmalige Anzeige) | use() mit gecachtem Promise + Suspense |
| Komplexer Client-State (Auto-Refetch, Pagination, Mutationen) | TanStack Query oder SWR |
| 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) | useSyncExternalStore |
| DOM-Messungen, Event-Listener, Timer | useEffect: dafür ist es tatsächlich gedacht |
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:
tsxfunction useUser(id: string) {const [user, setUser] = useState<User | null>(null);const [isLoading, setIsLoading] = useState(true);const [error, setError] = useState<Error | null>(null);useEffect(() => {let cancelled = false;setIsLoading(true);fetchUser(id).then((data) => {if (!cancelled) setUser(data);}).catch((err) => {if (!cancelled) setError(err);}).finally(() => {if (!cancelled) setIsLoading(false);});return () => {cancelled = true;};}, [id]);return { user, isLoading, error };}
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<ErrorBoundary fallback={<ErrorMessage />}><Suspense fallback={<Skeleton />}><UserProfile userId={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:
tsxfunction UserProfile({ userPromise }: { userPromise: Promise<User> }) {const user = use(userPromise);return <ProfileCard user={user} />;}
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 abstuerzentry {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:
tsxconst safePromise = riskyFetch().catch(() => defaultValue);const data = use(safePromise);
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:
tsx// Server Componentimport { Suspense } from "react";import { ErrorBoundary } from "react-error-boundary";import { fetchUser, fetchOrders } from "@/lib/api";import { Dashboard } from "./Dashboard";export default function DashboardPage({ params }: { params: { id: string } }) {// Beide Fetches starten gleichzeitig - kein Wasserfallconst userPromise = fetchUser(params.id);const ordersPromise = fetchOrders(params.id);return (<ErrorBoundary fallback={<p>Etwas ist schiefgelaufen.</p>}><Suspense fallback={<HeaderSkeleton />}><Dashboard userPromise={userPromise} ordersPromise={ordersPromise} /></Suspense></ErrorBoundary>);}
tsx// Client Component"use client";import { use, Suspense } from "react";export function Dashboard({userPromise,ordersPromise,}: {userPromise: Promise<User>;ordersPromise: Promise<Order[]>;}) {const user = use(userPromise);return (<div><h1>Willkommen zurück, {user.name}</h1><Suspense fallback={<OrdersSkeleton />}><OrderList ordersPromise={ordersPromise} /></Suspense></div>);}function OrderList({ ordersPromise }: { ordersPromise: Promise<Order[]> }) {const orders = use(ordersPromise);if (orders.length === 0) return <p>Keine aktuellen Bestellungen.</p>;return (<ul>{orders.map((order) => (<li key={order.id}>{order.item} – {order.status}</li>))}</ul>);}
Was zur Laufzeit passiert
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.
tsx"use client";function DetailPopup({ dataPromise }: { dataPromise: Promise<ItemDetail> }) {const detail = use(dataPromise);return <div>{detail.description}</div>;}function ItemCard({ itemId }: { itemId: string }) {const [promise, setPromise] = useState<Promise<ItemDetail> | null>(null);const handleOpen = () => {setPromise(fetchItemDetail(itemId)); // Fetch startet sofort};return (<><button onClick={handleOpen}>Details anzeigen</button>{promise && (<Suspense fallback={<Skeleton />}><DetailPopup dataPromise={promise} /></Suspense>)}</>);}
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:
tsconst cache = new Map<string, Promise<ItemDetail>>();export function getItemDetail(id: string) {if (!cache.has(id)) {cache.set(id,fetch(`/api/items/${id}`).then((r) => r.json()),);}return cache.get(id)!;}
tsxconst handleOpen = () => {setPromise(getItemDetail(itemId));};
Info
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.
Quellen & Weiterführende Links
- use: React Referenz
Die offizielle React-Dokumentation für den use()-Hook. API-Referenz, Regeln, Einschränkungen und Beispiele mit Promises und Context.
- Suspense: React Referenz
Wie Suspense-Boundaries funktionieren, verschachtelte Boundaries, gestaffeltes Laden und Integration mit Streaming Server Rendering.
- You Might Not Need an Effect: React Docs
Reacts offizieller Guide zum Vermeiden unnötiger useEffect-Aufrufe. Pflichtlektüre um zu verstehen, wann use() oder useMemo die bessere Wahl ist.
- cache: React Referenz
Reacts eingebaute cache()-Funktion zur Deduplizierung von Data Fetches über Server-Komponenten innerhalb eines einzelnen Requests.
- TanStack Query
Die populärste clientseitige Data-Fetching-Bibliothek für React, übernimmt Caching, Background-Refetching, Pagination und optimistische Updates.
- React Has Changed, Your Hooks Should Too: Matt Smith
Ein praktischer Überblick darüber, wie React 18/19 die Art verändert, wie Entwickler über Hooks, Effects und Data-Fetching-Architektur denken sollten.
