# Suspense und der use-Hook: rendern, als wären die Daten schon da

URL: https://www.mikebild.dev/de/blog/react-suspense-und-use/

Wer das Warten in jeder Komponente einzeln ausprogrammiert, bezahlt das an der Oberfläche: Drei datenladende Komponenten nebeneinander heißen drei Spinner, die unabhängig voneinander aufblitzen und zu drei verschiedenen Zeitpunkten wieder verschwinden. Die Seite zuckt beim Aufbau, und jede dieser Komponenten schleppt dieselben drei Fälle mit sich herum – lädt, Fehler, Daten. In jeder steht irgendwo `if (isLoading) return <Spinner />` und darunter `if (error) return <ErrorMessage />`.

Daran ändert auch der Umzug auf TanStack Query oder SWR nichts. Im [Beitrag über Client-Zustand und Server-Cache](https://www.mikebild.dev/de/blog/client-zustand-und-server-cache/) habe ich gezeigt, wie Serverdaten aus dem Gebastel aus `useState` und `useEffect` herauswandern und einer Cache-Bibliothek zufallen – der Ladezustand wandert dabei nicht mit, er bleibt in jeder Komponente stehen, die Daten braucht. React 19 setzt an einer anderen Stelle an, nämlich bei der Frage, wo im Baum das Warten überhaupt stattfindet. Suspense dreht sie um: Das Warten gehört nicht mehr in jede einzelne Komponente, sondern an eine Stelle darüber. Und die Komponente darunter darf so tun, als seien die Daten schon da.

**Redaktionelle Einordnung (Juli 2026):** Suspense reagiert nicht auf beliebiges Datenladen in einem Effect oder Event-Handler. Für Daten funktioniert der hier beschriebene Ablauf mit Suspense-fähigen Frameworks und Bibliotheken oder beim Lesen eines stabilen, gecachten Promise mit `use`. Eine frei nutzbare, stabile React-API zum Bau eigener Suspense-Datenquellen ist weiterhin nicht dokumentiert.

## Der Ladezustand verlässt die Komponente

Sehen wir uns zuerst an, wie es ohne Suspense aussieht. Eine Komponente, die eine Liste von Rechnungen anzeigt, trägt die drei Zustände selbst:

```jsx
function InvoiceList() {
  const { data, isLoading, error } = useInvoices();

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <ul>
      {data.map((invoice) => (
        <li key={invoice.id}>{invoice.title}</li>
      ))}
    </ul>
  );
}
```

Das ist nicht schlimm, aber es wiederholt sich. Jede datenladende Komponente sieht so aus, jede hat ihren eigenen `Spinner`, ihre eigene Fehlerbehandlung. Und wenn drei solche Komponenten nebeneinander stehen, blitzen drei Spinner unabhängig voneinander auf und verschwinden zu unterschiedlichen Zeitpunkten – ein unruhiges Bild.

Suspense stellt dem eine andere Idee gegenüber. Man umschließt einen Teil des Baums einmal mit `<Suspense>` und sagt: Alles darin, das noch auf Daten wartet, soll so lange diesen Fallback zeigen.

```jsx
<Suspense fallback={<Spinner />}>
  <InvoiceList />
</Suspense>
```

Die `InvoiceList` selbst kennt jetzt keinen Ladezustand mehr. Sie fragt die Daten ab, und wenn die noch nicht da sind, hält React die Komponente an und zeigt stattdessen den Fallback der nächsten umschließenden `<Suspense>`-Komponente. Das Wort dafür ist *suspendieren*: Die Komponente pausiert mitten im Rendern, bis das Versprochene eintrifft. Der Fallback ist nicht mehr Sache der Komponente, sondern des `<Suspense>` darüber.

## Der `use`-Hook liest ein Promise

Damit eine Komponente überhaupt suspendieren kann, braucht sie eine Möglichkeit, ein noch offenes Versprechen zu „lesen“. Genau das macht der `use`-Hook, der mit React 19 stabil ist. Man übergibt ihm ein Promise, und er gibt den aufgelösten Wert zurück – oder unterbricht das Rendern, wenn der Wert noch nicht da ist.

```jsx
import { use } from "react";

function InvoiceList({ invoicesPromise }) {
  const invoices = use(invoicesPromise);

  return (
    <ul>
      {invoices.map((invoice) => (
        <li key={invoice.id}>{invoice.title}</li>
      ))}
    </ul>
  );
}
```

Man liest diese Zeile fast wie synchronen Code: `const invoices = use(invoicesPromise)`, danach steht `invoices` einfach zur Verfügung. Von der Zwischenzeit, in der das Promise noch offen ist, sieht die Komponente nichts. Ist das Promise noch nicht erfüllt, suspendiert `use` die Komponente, React hält an und zeigt den Fallback der umschließenden `<Suspense>`-Komponente. Sobald das Promise erfüllt ist, rendert React die Komponente erneut, diesmal mit dem Wert in der Hand.

Neben Promises nimmt `use` übrigens auch einen Context entgegen und ersetzt in dieser Rolle `useContext`. Der interessante Fall für diesen Beitrag ist aber das Promise, weil daran das Zusammenspiel mit Suspense hängt.

## Warum `use` bedingt aufgerufen werden darf

Hier lohnt sich ein kurzer Blick auf eine Besonderheit, die auf den ersten Blick wie ein Regelbruch aussieht. Für alle anderen Hooks gilt eine eiserne Regel: Sie müssen auf oberster Ebene der Komponente stehen, in immer derselben Reihenfolge, nie in einer Bedingung oder Schleife. Der Grund ist, dass React die Hooks pro Render über ihre Aufrufreihenfolge zuordnet – gerät die Reihenfolge durcheinander, verwechselt React die Zustände.

`use` ist ausdrücklich von dieser Regel ausgenommen. Man darf ihn in einer `if`-Bedingung aufrufen, in einer Schleife, hinter einem frühen `return`:

```jsx
function Message({ show, messagePromise }) {
  if (show) {
    const message = use(messagePromise);
    return <p>{message}</p>;
  }
  return null;
}
```

Das ist mehr als eine Bequemlichkeit. `use` speichert selbst keinen Zustand, der über eine feste Aufrufreihenfolge einer Komponente zugeordnet werden müsste – er liest nur einen Wert aus einem Objekt, das man ihm reicht. Deshalb kann er dort stehen, wo er inhaltlich hingehört, statt an den Anfang der Komponente gezwungen zu werden. Man ruft ein Promise genau dann ab, wenn man seinen Wert wirklich braucht.

## Fehler übernimmt die Error Boundary

Bleibt der dritte Zustand von oben: der Fehler. Wenn das Promise nicht erfüllt, sondern abgelehnt wird, wirft `use` diesen Fehler beim Rendern. Und ein geworfener Fehler landet bei der nächsten *Error Boundary* – demselben Mechanismus, mit dem React seit jeher Render-Fehler auffängt.

```jsx
<ErrorBoundary fallback={<ErrorMessage />}>
  <Suspense fallback={<Spinner />}>
    <InvoiceList invoicesPromise={invoicesPromise} />
  </Suspense>
</ErrorBoundary>
```

Damit ist die Rechnung aufgegangen. Aus den drei Zuständen, die vorher jede Komponente selbst verwaltet hat – lädt, Fehler, Daten –, sind zwei Auffangnetze im Baum geworden plus der reine Erfolgsfall in der Komponente. Das `<Suspense>` kümmert sich um das Warten, die `<ErrorBoundary>` um das Scheitern, und die Komponente selbst rendert nur noch den Fall, für den sie eigentlich da ist: die vorhandenen Daten. Das `if (isLoading)` und das `if (error)` sind ersatzlos verschwunden.

```mermaid
flowchart TB
  R["Komponente rendert,<br/>ruft use(promise)"] --> Q{"Promise-Zustand?"}
  Q -->|offen| S["suspendiert →<br/>Suspense zeigt Fallback"]
  S -.->|Promise erfüllt| R
  Q -->|erfüllt| OK["Inhalt erscheint<br/>mit den Daten"]
  Q -->|abgelehnt| ERR["Fehler wird geworfen →<br/>ErrorBoundary fängt ihn"]
```

An dieser Stelle ist eine Error Boundary noch eine Klassenkomponente oder man greift zu einem kleinen Paket wie `react-error-boundary` – eine eingebaute Funktions-Variante gibt es in React 19 noch nicht. Das ändert am Bild aber nichts: konzeptionell ist es das zweite Auffangnetz neben Suspense.

## Ehrlich bleiben: `use` ist keine Cache-Bibliothek

Jetzt kommt der Teil, den man leicht überliest, weil die Beispiele oben so aufgeräumt wirken. Woher kommt eigentlich das `invoicesPromise`? In den Codeschnipseln habe ich es als Prop hereingereicht und die Frage damit verschoben. Sie ist aber die entscheidende.

Man könnte auf die Idee kommen, das Promise einfach in der Komponente zu erzeugen:

```jsx
function InvoiceList() {
  // Trap: creates a new promise on EVERY render
  const invoices = use(fetch("/api/invoices").then((r) => r.json()));
  // ...
}
```

Das sieht kompakt aus und ist trotzdem falsch. Jeder Render-Durchlauf ruft `fetch` erneut auf und erzeugt ein frisches Promise. React sieht ein noch offenes Promise, suspendiert, rendert nach dem Auflösen neu – und erzeugt dabei sofort das nächste neue Promise. Man baut sich eine Endlosschleife aus Ladevorgängen. `use` cacht nichts, es dedupliziert nichts, es *liest* nur, was man ihm gibt.

Damit das funktioniert, muss das Promise von außerhalb des Renderns kommen: von jemandem, der es einmal erzeugt, unter einem Schlüssel ablegt und bei der nächsten Anfrage dasselbe Promise zurückgibt. Das ist genau die Buchhaltung – Schlüssel, Alter, Deduplizierung, Invalidierung –, die ich im [Beitrag über den Server-Cache](https://www.mikebild.dev/de/blog/client-zustand-und-server-cache/) beschrieben habe. `use` und Suspense lösen sie nicht, sie setzen sie voraus.

In der Praxis liefern deshalb dieselben Werkzeuge die Promises, die man ohnehin schon einsetzt. TanStack Query und SWR haben beide einen Suspense-Modus: Statt `isLoading` und `error` zurückzugeben, suspendieren sie und werfen im Fehlerfall, sodass Suspense und Error Boundary die Zustände übernehmen. Oder ein Framework wie Next.js erzeugt die Promises auf dem Server und reicht sie hinein. Die Regel aus dem Cache-Beitrag gilt unverändert: Serverdaten brauchen einen Cache mit Schlüssel und Invalidierung. Suspense ändert nur, wie sich das Warten darauf im Baum anfühlt – nicht, dass jemand den Cache führen muss.

## Wofür sich der Aufwand lohnt

Wenn `use` die Buchhaltung nicht abnimmt, warum dann der ganze Umbau? Der Gewinn liegt woanders, nämlich in der Koordination mehrerer Datenquellen unter einem gemeinsamen `<Suspense>`.

Stellen wir uns eine Profilseite vor, die Nutzerdaten, die letzten Beiträge und eine Statistik lädt – drei Quellen. Ohne Suspense blitzen drei Spinner unabhängig auf und lösen sich zu drei Zeitpunkten auf; die Seite zuckt beim Aufbau. Legt man die drei Komponenten unter ein gemeinsames `<Suspense>`, wartet React, bis alle drei so weit sind, und zeigt dann die fertige Seite in einem Stück. Ein Fallback statt drei, ein Moment des Erscheinens statt drei.

Genauso oft will man das Gegenteil: nicht auf die langsamste Quelle warten, sondern das Schnelle sofort zeigen. Dann verschachtelt man mehrere `<Suspense>`-Komponenten. Das Nutzerprofil steht unter einem äußeren `<Suspense>` und erscheint früh; die langsame Statistik bekommt ihr eigenes inneres `<Suspense>` mit eigenem Fallback und schiebt sich nach, sobald sie da ist.

```jsx
<Suspense fallback={<PageSkeleton />}>
  <UserProfile userPromise={userPromise} />
  <Suspense fallback={<StatsSkeleton />}>
    <Statistics statsPromise={statsPromise} />
  </Suspense>
</Suspense>
```

So beschreibt man ein gestaffeltes Erscheinen als Struktur im Baum, nicht als Logik in den Komponenten. Wo etwas erscheint und in welcher Reihenfolge, liest man an der Verschachtelung der `<Suspense>`-Komponenten ab – an einer Stelle, statt über ein Dutzend `isLoading`-Abfragen verstreut. Das ist der eigentliche Zugewinn: Die Lade-UI liegt dort, wo man über Layout nachdenkt, und nicht in jeder Komponente noch einmal.

Damit hängt ein zweiter, weniger sichtbarer Vorteil zusammen: Suspense wirkt Lade-Wasserfällen entgegen. Ein Wasserfall entsteht, wenn eine Komponente erst lädt, dann rendert, und ein Kind darin wiederum erst dann mit seinem eigenen Laden beginnt – die Anfragen reihen sich nacheinander auf, obwohl sie voneinander gar nicht abhängen. Genau dieses Muster produziert das klassische `useEffect`-Laden fast von selbst, weil jede Komponente erst dann fetcht, wenn sie gerendert wird, und Kinder erst rendern, wenn die Eltern fertig geladen haben. Reicht man die Promises dagegen von außen hinein und startet sie früh, laufen die Anfragen parallel, während `use` sie unter einem gemeinsamen `<Suspense>` wieder einsammelt. Die Struktur des Wartens und die Struktur des Ladens fallen nicht mehr zwangsläufig zusammen.

## Ein Nachladen, das die alte UI stehen lässt

Ein Detail sei noch angerissen, weil es in der Praxis schnell wichtig wird. Suspense zeigt seinen Fallback nicht nur beim ersten Laden, sondern auch, wenn sich die Daten unter einem `<Suspense>` ändern – etwa wenn man von einer Rechnung zur nächsten wechselt und ein neues Promise entsteht. Ohne Vorkehrung springt die Seite dann zurück in den Spinner, obwohl gerade eben noch brauchbare Daten zu sehen waren.

Genau dafür gibt es `useTransition` und `startTransition`. Markiert man den Datenwechsel als Übergang, hält React die alte Oberfläche stehen und lädt im Hintergrund nach, statt sofort in den Fallback zu fallen. Man kann währenddessen anzeigen, dass etwas im Gange ist – ausgegraut, mit einem dezenten Hinweis –, ohne den bereits gerenderten Inhalt wegzuwerfen. Das verdient einen eigenen Beitrag; hier soll nur klar sein, dass das harte Zurückspringen in den Fallback kein Zwang ist.

## Was bleiben soll

Suspense und `use` verschieben nicht, wer die Daten besorgt – das bleibt Sache eines Caches. Sie verschieben, wo das Warten beschrieben wird. Statt in jeder Komponente drei Zustände auszuprogrammieren, zieht man Laden und Fehler an zwei Stellen im Baum zusammen – Suspense und Error Boundary – und lässt die Komponenten darunter den Erfolgsfall annehmen. Die einzelne Komponente wird dadurch kleiner und ehrlicher: Sie tut so, als seien die Daten da, weil ein `<Suspense>` über ihr dafür sorgt, dass sie es sind, wenn die Komponente an die Reihe kommt.

Beim Entwurf einer Seite bleibt damit eine Frage übrig: An welchen Stellen im Baum darf gewartet werden, und was soll dort so lange zu sehen sein? Die Antwort sind ein paar `<Suspense>`-Komponenten an den richtigen Stellen – und Komponenten, die vom Warten nichts mehr wissen müssen.

## Weiterführende Quellen

- Repository introduction-react (Kapitel server-access, react-hooks; Ordner recipes zum Data Fetching): https://github.com/mikebild/introduction-react
- React, „Suspense“: https://react.dev/reference/react/Suspense
- React, „use“: https://react.dev/reference/react/use
- React, Suspense-fähiges Data Fetching: https://react.dev/reference/react/Suspense#displaying-a-fallback-while-content-is-loading
