</>
{ }
React 19 · Suspense · Hooks · Datenladen
React use()-Hook
Promises direkt in Komponenten auflösen

Daten in React zu laden bedeutete bislang immer: useEffect, useState, Lade-State, Error-State, Boilerplate. Der use()-Hook aus React 19 macht das deklarativ: Ein Promise rein, der aufgelöste Wert raus – Suspense übernimmt den Rest.

11 Min. Lesezeit use() · Suspense · ErrorBoundary · Context · Server Components React 19+ · TypeScript · Next.js

1. Das Boilerplate-Problem beim Daten-Laden

Das Laden von Daten in React-Komponenten war seit den Anfängen mit Boilerplate verbunden. Das Standardmuster: useState für die Daten, useState für den Lade-Status, useState für den Fehler, und ein useEffect, der den Fetch auslöst, den Cleanup-Fall behandelt (AbortController), die States setzt und den Dependency-Array korrekt befüllt. Für eine einfache Liste von Produkten entstehen so schnell 25 Zeilen Boilerplate, bevor die erste Geschäftslogik beginnt.

Bibliotheken wie TanStack Query (React Query) und SWR haben dieses Problem auf Anwendungsebene gelöst. Sie abstrahieren Caching, Refetching, Background-Updates und Error-Handling in wiederverwendbare Hooks. Das ist ein echter Gewinn, aber es ist auch eine externe Abhängigkeit für etwas, das konzeptionell sehr einfach ist: Warte auf ein Promise, zeige den aufgelösten Wert. Genau das ist der Ausgangspunkt für den use()-Hook in React 19.

Ein weiteres Problem des klassischen Ansatzes: Der Lade-Zustand wird lokal in jeder Komponente verwaltet. Wenn mehrere Komponenten gleichzeitig laden, gibt es mehrere Spinner an verschiedenen Stellen der Seite. Suspense adressiert dieses Problem auf Strukturebene: Eine Suspense-Boundary koordiniert den Lade-Zustand für alle Kind-Komponenten und zeigt genau einen Fallback an, während irgendeine Kind-Komponente noch lädt. use() ist der Mechanismus, mit dem Komponenten in dieses Suspense-System einsteigen.

2. Suspense: Deklaratives Laden in React

Suspense wurde in React 16.6 eingeführt, war aber lange auf React.lazy beschränkt. Mit React 18 wurde Suspense für Daten-Laden freigegeben, aber die offizielle API fehlte noch. React 19 schließt diese Lücke: use() ist die offizielle API, mit der eine Komponente "suspendieren" kann – also ihren Render pausieren, bis ein Promise erfüllt ist, ohne dass der Nutzer eine Lade-UI in der Komponente selbst implementieren muss.

Das Prinzip: Eine Komponente wirft intern eine Exception (ein Promise), wenn sie noch lädt. Die nächste übergeordnete <Suspense>-Boundary fängt diese Exception und rendert stattdessen den fallback. Sobald das Promise erfüllt ist, versucht React erneut, die Komponente zu rendern – diesmal liefert use() den aufgelösten Wert und die Komponente rendert normal. Dieses Werfen und Wiederholen passiert intern, der Entwickler sieht nur einen synchron wirkenden Lesezugriff auf das Promise.

Mehrere Komponenten unter derselben Suspense-Boundary koordinieren ihren Lade-Zustand gemeinsam. Wenn sowohl UserProfile als auch OrderHistory unter derselben Boundary sind, wartet React, bis beide fertig sind, bevor es beide rendert. Das verhindert "Popcorn-UI" – ein visuelles Flackern, bei dem einzelne Komponenten nacheinander erscheinen. Mit geschachtelten Suspense-Boundaries kann dieses Verhalten gezielt gesteuert werden.

3. use(): Syntax und grundlegendes Verhalten

use() hat eine ungewöhnliche Eigenschaft unter den React-Hooks: Es kann bedingt aufgerufen werden. Die klassische Regel "Hooks dürfen nicht in Bedingungen aufgerufen werden" gilt für use() nicht. Das ist absichtlich: Da use() auf ein Promise oder einen Context wartet und nicht selbst State verwaltet, gibt es keinen Grund für die Reihenfolge-Einschränkung. Man kann use() also innerhalb von if-Blöcken, try-Blöcken oder nach Early-Returns aufrufen – was für Context-Lesezugriffe besonders nützlich ist.

Das grundlegende Verhalten: const data = use(somePromise) löst das Promise synchron (aus der Komponenten-Perspektive) auf und gibt den Wert zurück. Wenn das Promise noch aussteht, suspendiert die Komponente. Wenn das Promise fehlschlägt, wird der Fehler an die nächste ErrorBoundary weitergegeben. Wenn das Promise bereits erfüllt ist (aus dem Cache), gibt use() sofort den Wert zurück, ohne zu suspendieren. Das Verhalten hängt also vom Zustand des Promises ab – nicht von einem separaten Lade-State.


import { use, Suspense } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

// Fetch function — returns a Promise, not the data directly
async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<User>;
}

// Component reads the resolved value with use() — suspends if pending
function UserCard({ userPromise }: { userPromise: Promise<User> }) {
  // use() suspends the component until the Promise resolves
  const user = use(userPromise);

  return (
    <div className="border rounded-lg p-4">
      <h2 className="font-bold">{user.name}</h2>
      <p className="text-slate-600">{user.email}</p>
    </div>
  );
}

// Parent creates the Promise and passes it as a prop
export function UserPage({ userId }: { userId: number }) {
  // Promise is created OUTSIDE the component that uses it
  // (or at a stable reference point — do not create inside the child)
  const userPromise = fetchUser(userId);

  return (
    // Suspense boundary shows fallback while UserCard suspends
    <Suspense fallback={<div className="animate-pulse h-16 bg-slate-100 rounded-lg" />}>
      <UserCard userPromise={userPromise} />
    </Suspense>
  );
}

4. Promises mit use() auflösen

Das Promise, das an use() übergeben wird, sollte stabil sein – also nicht bei jedem Render neu erstellt werden. Wenn das Promise bei jedem Render neu erstellt wird, suspendiert die Komponente jedes Mal neu, was zu einem unendlichen Lade-Loop führt. Das korrekte Muster: Das Promise wird in der Eltern-Komponente erstellt (oder aus einem Cache geholt) und als Prop an die Kind-Komponente übergeben, die use() aufruft. In Server Components kann das Promise direkt in der Komponente erstellt werden, da Server Components bei einer Anfrage genau einmal rendern.

In Client-Komponenten empfiehlt es sich, Promises in einem Cache oder einer Bibliothek wie TanStack Query zu verwalten. Das verhindert das erneute Auslösen von Requests bei Re-Renders. Eine einfache Alternative für unkritische Fälle: Das Promise mit useState in der Eltern-Komponente speichern. useState initialisiert den State nur einmal, sodass das Promise stabil bleibt. const [userPromise] = useState(() => fetchUser(userId)) – das Factory-Argument von useState wird nur beim ersten Render ausgeführt.

Das Caching-Muster ist entscheidend für korrekte Anwendungen. Wenn eine Komponente mit use() suspendiert und nach der Auflösung neu rendert, muss das Promise beim zweiten Render dasselbe sein wie beim ersten – sonst holt React neue Daten und die Komponente suspendiert erneut. In Next.js kümmert sich das Framework um dieses Caching durch den Server-Components-Renderer. In reinen Client-Anwendungen ist eine dedizierte Caching-Schicht (TanStack Query, SWR oder ein eigenes Cache-Objekt) notwendig.

5. Fehlerbehandlung mit ErrorBoundary

Wenn das Promise, das an use() übergeben wird, rejected wird (also fehlschlägt), gibt use() den Fehler an die nächste ErrorBoundary weiter. Eine ErrorBoundary ist eine Klassen-Komponente, die den componentDidCatch-Lifecycle implementiert und beim Fehler einen Fehler-Fallback rendert. React hat keine eingebaute ErrorBoundary-Komponente, aber es gibt eine weit verbreitete Bibliothek namens react-error-boundary, die diese Funktionalität bereitstellt.

Das Muster für robuste Datenladekomponenten: Jede Suspense-Boundary wird von einer ErrorBoundary umschlossen. Die ErrorBoundary fängt sowohl Fehler aus fehlgeschlagenen Promises (via use()) als auch Render-Fehler der Kind-Komponenten ab. Der Fallback der ErrorBoundary kann einen "Erneut versuchen"-Button enthalten, der die ErrorBoundary zurücksetzt und die Komponente neu rendert – was das fehlgeschlagene Promise neu auslöst.

Ein häufiges Missverständnis: try/catch innerhalb einer Komponente fängt keine Fehler ab, die durch use() geworfen werden. Da das Werfen intern durch React geschieht (nach dem Promise-Rejection), ist try/catch im Komponenten-Body wirkungslos. Fehlerbehandlung muss immer über ErrorBoundaries erfolgen. Das ist dasselbe Verhalten wie bei throw in Render-Funktionen allgemein – React fängt Render-Fehler nur über ErrorBoundaries ab.


import { use, Suspense, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

interface Product {
  id: number;
  title: string;
  price: number;
}

async function fetchProducts(category: string): Promise<Product[]> {
  const res = await fetch(`/api/products?category=${encodeURIComponent(category)}`);
  if (!res.ok) throw new Error(`Failed to load products: ${res.status}`);
  return res.json() as Promise<Product[]>;
}

// use() suspends until products are loaded — errors bubble to ErrorBoundary
function ProductList({ productsPromise }: { productsPromise: Promise<Product[]> }) {
  const products = use(productsPromise);

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.title} — {p.price}€</li>
      ))}
    </ul>
  );
}

// Error fallback with retry capability
function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
  return (
    <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
      <p className="text-red-700 font-semibold">Fehler beim Laden: {error.message}</p>
      <button onClick={resetErrorBoundary} className="mt-2 text-sm text-red-600 underline">
        Erneut versuchen
      </button>
    </div>
  );
}

export function CategoryPage({ category }: { category: string }) {
  // Stable Promise reference — created once via useState factory
  const [productsPromise] = useState(() => fetchProducts(category));

  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Suspense fallback={<p className="text-slate-400">Produkte werden geladen …</p>}>
        <ProductList productsPromise={productsPromise} />
      </Suspense>
    </ErrorBoundary>
  );
}

6. Context mit use() lesen

use() kann nicht nur Promises, sondern auch React-Context lesen. Im Unterschied zu useContext kann use(context) bedingt aufgerufen werden – in if-Blöcken, nach Early-Returns, oder innerhalb von Schleifen. Das ist besonders nützlich, wenn man Context nur unter bestimmten Bedingungen lesen möchte, ohne den Aufruf immer an den Anfang der Komponente stellen zu müssen. Das Verhalten ist ansonsten identisch mit useContext: Die Komponente abonniert den Context und rendert neu, wenn sich der Context-Wert ändert.

Ein praktisches Beispiel: Eine Komponente, die einen User-Context nur liest, wenn ein requiresAuth-Prop gesetzt ist. Mit useContext muss der Context-Aufruf immer am Anfang stehen, auch wenn er im aktuellen Render-Durchlauf nicht benötigt wird. Mit use() kann er nach der Bedingung platziert werden. Das macht den Code lesbarer und macht die bedingte Natur des Context-Zugriffs explizit. Diese Flexibilität ist der einzige Unterschied zu useContext – beide sind äquivalent, wenn der Context immer gelesen wird.

In Server Components kann use(context) nicht verwendet werden – Server Components können keinen Context lesen (da Context an einen Client-Render-Baum gebunden ist). Für das Übergeben von Werten an Server Components muss man Props oder Server-seitige Mechanismen wie Cookies und Datenbank-Abfragen nutzen. use(context) ist rein für Client-Komponenten gedacht und verhält sich dort identisch zu useContext – plus der bedingten Aufruf-Flexibilität.

7. use() vs. useEffect+useState im Vergleich

Der klassische useEffect-Ansatz für Daten-Laden hat fundamentale Schwächen, die use() adressiert. Das wichtigste: useEffect läuft nach dem ersten Render. Das bedeutet, dass die Komponente immer zweimal rendert: einmal mit leerem State (und explizitem Lade-Spinner in der Komponente selbst), einmal mit den geladenen Daten. use() suspendiert vor dem ersten Render, sodass die Komponente nur rendert, wenn die Daten bereit sind – der Lade-Zustand liegt ausschließlich in der Suspense-Boundary.

Kriterium useEffect + useState use() + Suspense
Boilerplate 3 useState + useEffect + Cleanup Ein Hook-Aufruf
Render-Phasen Immer mindestens 2 Renders Direkt mit Daten
Lade-UI In jeder Komponente separat Zentral in Suspense-Boundary
Fehlerbehandlung Manueller error-State Automatisch via ErrorBoundary
Race Conditions AbortController nötig Kein Race-Condition-Risiko

Race Conditions sind ein besonders heimtückisches Problem des useEffect-Ansatzes. Wenn der Nutzer schnell zwischen Tabs wechselt und mehrere Requests ausgelöst werden, können die Antworten in falscher Reihenfolge ankommen. Das letzt zurückgekommene Ergebnis überschreibt das aktuelle, obwohl es von einer älteren Anfrage stammt. Mit use() und einem stabilen Promise ist das kein Problem: Neue Daten werden durch ein neues Promise repräsentiert, und das alte wird verworfen.

8. Promise-Caching: Warum und wie

Wenn ein Komponenten-Baum neu rendert und dabei ein neues Promise an use() übergeben wird, suspendiert die Komponente erneut und zeigt den Suspense-Fallback. Das ist korrekt, aber in der Praxis unerwünscht: Man möchte nicht bei jedem Re-Render neu laden. Deshalb ist eine Caching-Schicht unerlässlich. Das Ziel: Dasselbe Promise für dieselbe Datenanfrage zurückgeben, solange die Daten noch frisch sind.

Die einfachste selbst geschriebene Cache-Implementierung nutzt eine Map, die Promise-Objekte nach einem Cache-Key speichert. Eine Funktion prüft, ob ein Promise für den Key bereits existiert; wenn ja, gibt sie das vorhandene zurück; wenn nein, erstellt sie ein neues und speichert es. Diese Cache-Funktion wird in der Eltern-Komponente aufgerufen, und das zurückgegebene Promise wird an die Kind-Komponente mit use() weitergegeben. Für Produktionsanwendungen ist TanStack Query oder ein ähnliches Framework die bessere Wahl, da es Caching, Invalidierung, Background-Refetching und Deduplizierung fertig mitbringt.

In Next.js mit App Router ist das Caching-Problem für Server Components automatisch gelöst: Das Next.js-Framework dedupliziert identische fetch()-Aufrufe innerhalb eines Render-Zyklus und cached Ergebnisse über den fetch-Cache. Server Components mit use() oder direktem await nutzen dieses Caching automatisch. In reinen Client-Anwendungen oder Remix ist eine explizite Caching-Strategie notwendig.