Paralleler Abruf in React: Suspense mit Promise.all und allSettled
Erfahren Sie, wie Sie Anfragen mit Promise.all entfernen, optionale Daten vor dem Zusammenbrechen von Seiten mit Promise.allSettled schützen und Ladezustände mit Suspense anzeigen können.
Eine Seite, die ständig Spinnerelemente anzeigt, weist in der Regel eines von zwei Problemen auf. Entweder werden die Anfragen nacheinander ausgeführt, obwohl sie gleichzeitig laufen könnten, oder eine optionale Anfrage, wie beispielsweise ein Empfehlungswidget, fehlschlägt und dadurch die gesamte Ansicht lahmlegt. Drei Werkzeuge beheben beide Probleme: Promise.all und Promise.allSettled ermöglichen das gleichzeitige Ausführen von Aufgaben mit unterschiedlichen Fehlerrichtlinien, während Reacts Suspense ein Ersatzelement anzeigt, solange die Daten noch ausstehen. In diesem Leitfaden wird jedes dieser Werkzeuge anhand eines kleinen Beispiels vorgestellt, bei dem zwei Listen von einer öffentlichen API geladen werden.
Ursprung der „Wasserfall-Ansicht“
Durch das Abwarten der Anrufe nacheinander werden diese seriell abgewickelt. Die zweite Anfrage beginnt erst, nachdem die erste abgeschlossen ist, sodass die Gesamtverzögerung aus der Summe beider Zeiten besteht:
const categories = await fetchCategories();
// fetchPosts will only start running after fetchCategories finishes executing.
const posts = await fetchPosts();
Falls die Anfragen nicht voneinander abhängen, starten Sie beide zunächst und warten Sie gemeinsam auf sie. Die Gesamtzeit sinkt dann auf etwa die Zeit der langsamsten Anfrage anstelle der Summe beider Zeiten. Für einen umfassenderen Vergleich dieser Kombinatoren siehe die Entscheidung zwischen Promise.all, Promise.race und sequenziellen awaits.
Suspense: deklarative Ladezustände
Suspense umschließt einen Teil des Baums und zeigt einen Fallback an, wie zum Beispiel ein Skelett oder die Meldung „Daten laden...“, während alles darin darauf wartet. Der Rest der Seite bleibt sichtbar, und es ist nicht mehr notwendig, für diesen Abschnitt ein manuell erstelltes loading-Flag zu verwenden.
In der Demo wartet eine asynchrone Funktion künstlich drei Sekunden lang, damit der Ladezustand sichtbar wird, feuert anschließend die Zauber- und Buchanfragen gleichzeitig ab und kombiniert sie mit Promise.all. Der resultierende Promise wird an Section1 übergeben, welcher ihn mit Reacts use-Hook entpackt. Solange der Promise nicht abgeschlossen ist, pausiert use den Komponenten und zeigt die Ersatzansicht an.
'use client';
import React, { Suspense, use } from 'react';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export default function Home() {
const fetchDataParalel = async () => {
await delay(3000); // Simulate a 3-second loading delay
// Trigger both APIs in PARALLEL (simultaneously)
const getSpellsPromise = fetch(
'https://potterapi-fedeperin.vercel.app/en/spells?max=3&page=1'
).then((res) => res.json());
const getBooksPromise = fetch(
'https://potterapi-fedeperin.vercel.app/en/books?max=3&page=1'
).then((res) => res.json());
// Wait for both to finish together
const [spellsData, booksData] = await Promise.all([
getSpellsPromise,
getBooksPromise,
]);
return {
spells: spellsData?.map((item: any) => item.spell) || [],
books: booksData?.map((item: any) => item.title) || [],
};
};
return (
<main className="p-4">
<p>
<strong>Demo Suspense + Promise.all</strong>
</p>
<br />
<Suspense fallback={<p>Loading data...</p>}>
<Section1 dataPromise={fetchDataParalel()} />
</Suspense>
</main>
);
}
function Section1({
dataPromise,
}: {
dataPromise: Promise<{ spells: string[]; books: string[] }>;
}) {
const data = use(dataPromise);
return (
<div>
<p>
<strong>Spells: </strong>{' '}
{data.spells.length ? data.spells.join(', ') : '-'}
</p>
<p>
<strong>Books: </strong>{' '}
{data.books.length ? data.books.join(', ') : '-'}
</p>
</div>
);
}
Eine Warnung: Dieser Komponente ruft während des Renderns fetchDataParalel() auf, wodurch jedes Mal, wenn Home gerendert wird, eine neue Promise erstellt wird. React erwartet von den an use übergebenen Promises, dass sie stabil sind – in einer Client-Komponente kann eine nicht gekachelte Promise Warnungen auslösen oder wiederholtes Abrufen verursachen. In Next.js wird die idiomatischere Variante verwendet, bei der die Promise in einer Server-Komponente erstellt wird (oder gekachtet wird) und anschließend weitergegeben wird, oder die entsprechende Section einfach als asynchrone Server-Komponente innerhalb von Suspense implementiert wird.
Promise.all: Alles oder nichts
Promise.all eignet sich für Daten, bei denen alle Felder erforderlich sind. Es gibt die Ergebnisse in der ursprünglichen Reihenfolge zurück, sobald alle Eingaben erfüllt sind, und lehnt ab, sobald eine Eingabe fehlschlägt. Die zweite Demonstration verwendet denselben parallelen Abruf, diesmal aus useEffect mit einem expliziten loading-Zustand anstelle von Suspense:
'use client';
import React, { Suspense, use } from 'react';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export default function HomePromiseAll() {
const [loading, setLoading] = React.useState(false);
const [spells, setSpells] = React.useState([]);
const [books, setBooks] = React.useState([]);
const fetchDataParalel = async () => {
setLoading(true);
await delay(3000);
const getSpellsPromise = fetch(
'https://potterapi-fedeperin.vercel.app/en/spells?max=3&page=1'
).then((res) => res.json());
const getBooksPromise = fetch(
'https://potterapi-fedeperin.vercel.app/en/books?max=3&page=1'
).then((res) => res.json());
const [spellsData, booksData] = await Promise.all([
getSpellsPromise,
getBooksPromise,
]);
setSpells(spellsData?.map((item: any) => item.spell) || []);
setBooks(booksData?.map((item: any) => item.title) || []);
setLoading(false);
};
React.useEffect(() => {
fetchDataParalel();
}, []);
return (
<main>
<h1>Demo Promise.all</h1>
<br />
{loading ? (
<p>Loading data...</p>
) : (
<Section1 spells={spells} books={books} />
)}
</main>
);
}
function Section1({ spells, books }: { spells: string[]; books: string[] }) {
return (
<div>
<p>
<strong>Spells: </strong> {spells.length ? spells.join(', ') : '-'}
</p>
<p>
<strong>Books: </strong> {books.length ? books.join(', ') : '-'}
</p>
</div>
);
}
Beachten Sie, dass, wenn eine der Anfragen fehlschlägt, die Ausführung niemals zu setLoading(false) gelangt, und die Seite „Lade Daten...“ für immer anzeigt. Umhüllen Sie den Codekörper mit try/finally und rendern Sie einen Fehlerzustand in einem catch-Blöck, damit der Fehler sichtbar wird und nicht ignoriert wird. Promise.all storniert außerdem keine anderen Anfragen, wenn eine fehlschlägt; es hört einfach auf, auf sie zu warten.
Promise.allSettled: Teilfehlschläge tolerieren
Promise.allSettled lehnt niemals ab. Es wartet auf alle Eingaben und gibt für jede Eingabe ein Objekt mit einem status zurück – entweder fulfilled (mit einem value) oder rejected (mit einem reason). Anschließend entscheiden Sie je nach Ergebnis, was angezeigt werden soll:
'use client';
import React from 'react';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export default function HomePromiseAllSettled() {
const [loading, setLoading] = React.useState(false);
const [spells, setSpells] = React.useState<string[]>([]);
const [books, setBooks] = React.useState<string[]>([]);
const fetchDataParalel = async () => {
setLoading(true);
await delay(3000);
const getSpellsPromise = fetch(
'https://potterapi-fedeperin.vercel.app/en/spells?max=3&page=1'
).then((res) => res.json());
const getBooksPromise = fetch(
'https://potterapi-fedeperin.vercel.app/en/books?max=3&page=1'
).then((res) => res.json());
const [spellsResult, booksResult] = await Promise.allSettled([
getSpellsPromise,
getBooksPromise,
]);
// check status 'fulfilled' and get from `.value`
const finalSpells =
spellsResult.status === 'fulfilled'
? spellsResult.value.map((item: any) => item.spell)
: [];
const finalBooks =
booksResult.status === 'fulfilled'
? booksResult.value.map((item: any) => item.title)
: [];
setSpells(finalSpells);
setBooks(finalBooks);
setLoading(false);
};
React.useEffect(() => {
fetchDataParalel();
}, []);
return (
<main>
<h1>Demo Promise.allSettled</h1>
<br />
{loading ? (
<p>Loading data...</p>
) : (
<Section1 spells={spells} books={books} />
)}
</main>
);
}
function Section1({ spells, books }: { spells: string[]; books: string[] }) {
return (
<div>
<p>
<strong>Spells: </strong> {spells.length ? spells.join(', ') : '-'}
</p>
<p>
<strong>Books: </strong> {books.length ? books.join(', ') : '-'}
</p>
</div>
);
}
Bei einem fehlgeschlagenen Abruf wird hier eine leere Liste zurückgegeben, während die Bücher weiterhin angezeigt werden. Das ist die richtige Vorgehensweise für sekundäre Inhalte, doch beachten Sie, was als Fehler gilt. fetch lehnt nur bei Netzwerkfehlern ab, und der Aufruf von .json() lehnt nur bei unparsebaren Inhalten ab. Ein HTTP 500, der JSON zurückgibt, gilt weiterhin als fulfilled; prüfen Sie daher innerhalb jeder Kette res.ok, wenn Sie Serverfehler als Abbrüche behandeln möchten.
Die richtige Kombination auswählen
- Verwenden Sie
Promise.all, wenn die Ansicht ohne alle Datenfragmente sinnlos ist, und kombinieren Sie es mit einer Fehlerbegrenzung oder einem expliziten Fehlerzustand. - Verwenden Sie
Promise.allSettled, wenn einige Abschnitte optional sind und eine teilweise Seite besser ist als keine. - Umhüllen Sie jeden eigenständig ladenden Abschnitt in seine eigene
Suspense-Begrenzung, damit langsame Daten nur ihren eigenen Bereich beeinträchtigen. - Halten Sie die Promises stabil: Erstellen Sie sie auf dem Server oder speichern Sie sie im Cache, nicht bei jeder Client-Renderung.
Zusammenfassung
Wasserfallarchitekturen und einzelne Ausfallspunkte sind architektonische Entscheidungen und keine Unvermeidlichkeiten. Durch gemeinsames Starten unabhängiger Aufgaben, bewusste Wahl einer Ausfallstrategie sowie die Verwaltung der Ladebereiche mit Suspense erhalten Nutzer eine Seite, die schnell angezeigt wird und bei Fehlverhalten einer Abhängigkeit angemessen abfällt.