In diesem Beitrag geht es darum, zu verstehen, wie Suspense funktioniert, was es tut und wie es in eine echte Web-App integriert werden kann. Wir werden untersuchen, wie wir Routing und Datenladen mit Suspense in React integrieren. Für das Routing werde ich reines JavaScript verwenden und für Daten meine eigene GraphQL-Bibliothek micro-graphql-react.
Falls Sie sich fragen, was es mit React Router auf sich hat: Es scheint großartig zu sein, aber ich hatte nie die Gelegenheit, es zu verwenden. Mein eigenes Nebenprojekt hat eine so einfache Routing-Anforderung, dass ich es immer selbst gemacht habe. Außerdem gibt uns die Verwendung von reinem JavaScript einen besseren Einblick, wie Suspense funktioniert.
Ein wenig Hintergrund
Lassen Sie uns über Suspense selbst sprechen. Kingsley Silas bietet eine umfassende Übersicht darüber, aber das Wichtigste zuerst: Es handelt sich immer noch um eine experimentelle API. Das bedeutet – und die React-Dokumentation sagt dasselbe – dass Sie sich noch nicht darauf verlassen sollten, wenn Sie produktionsreife Arbeit leisten. Es besteht immer die Möglichkeit, dass es sich zwischen jetzt und der vollständigen Fertigstellung ändert, also behalten Sie das bitte im Hinterkopf.
Nichtsdestotrotz geht es bei Suspense darum, eine konsistente Benutzeroberfläche angesichts asynchroner Abhängigkeiten aufrechtzuerhalten, wie z. B. Lazy-Loaded-React-Komponenten, GraphQL-Daten usw. Suspense bietet Low-Level-APIs, mit denen Sie Ihre Benutzeroberfläche problemlos verwalten können, während Ihre App diese Dinge verwaltet.
Aber was bedeutet "konsistent" in diesem Fall? Es bedeutet, keine teilweise vollständige Benutzeroberfläche zu rendern. Es bedeutet, wenn drei Datenquellen auf der Seite vorhanden sind und eine davon abgeschlossen ist, möchten wir diesen aktualisierten Teil des Zustands nicht mit einem Spinner neben den veralteten anderen beiden Teilen des Zustands rendern.

Was wir tun möchten, ist dem Benutzer anzuzeigen, dass Daten geladen werden, während weiterhin entweder die alte Benutzeroberfläche oder eine alternative Benutzeroberfläche angezeigt wird, die darauf hinweist, dass wir auf Daten warten; Suspense unterstützt beides, was ich näher erläutern werde.

Was genau macht Suspense
Das ist alles weniger kompliziert, als es scheint. Traditionell in React haben Sie den Zustand gesetzt und Ihre Benutzeroberfläche hat sich aktualisiert. Das Leben war einfach. Aber es führte auch zu den oben beschriebenen Inkonsistenzen. Was Suspense hinzufügt, ist die Fähigkeit einer Komponente, React zum Zeitpunkt des Renderns zu benachrichtigen, dass sie auf asynchrone Daten wartet; dies nennt man Suspendieren, und es kann überall im Baum einer Komponente auftreten, so oft wie nötig, bis der Baum bereit ist. Wenn eine Komponente suspendiert, verzichtet React darauf, die ausstehende Zustandsaktualisierung zu rendern, bis alle ausstehenden Abhängigkeiten erfüllt sind.
Was passiert also, wenn eine Komponente suspendiert? React blickt den Baum hinauf, findet die erste <Suspense>-Komponente und rendert ihren Fallback. Ich werde viele Beispiele geben, aber vorerst wissen Sie, dass Sie dies bereitstellen können
<Suspense fallback={<Loading />}>
…und die <Loading />-Komponente wird gerendert, wenn eine untergeordnete Komponente von <Suspense> suspendiert ist.
Aber was ist, wenn wir bereits eine gültige, konsistente Benutzeroberfläche haben und der Benutzer neue Daten lädt, was dazu führt, dass eine Komponente suspendiert? Dies würde dazu führen, dass die gesamte bestehende Benutzeroberfläche ungerendert wird und der Fallback angezeigt wird. Das wäre immer noch konsistent, aber kaum eine gute Benutzererfahrung. Wir würden bevorzugen, dass die alte Benutzeroberfläche auf dem Bildschirm bleibt, während die neuen Daten geladen werden.
Um dies zu unterstützen, stellt React eine zweite API namens `useTransition` bereit, die effektiv eine Zustandsänderung im Speicher vornimmt. Mit anderen Worten, sie ermöglicht es Ihnen, den Zustand im Speicher festzulegen, während Sie Ihre vorhandene Benutzeroberfläche auf dem Bildschirm beibehalten. React rendert buchstäblich eine zweite Kopie Ihres Komponententeams im Speicher und legt den Zustand auf diesem Team fest. Komponenten können suspendieren, aber nur im Speicher, sodass Ihre vorhandene Benutzeroberfläche weiterhin auf dem Bildschirm angezeigt wird. Wenn die Zustandsänderung abgeschlossen ist und alle Suspensionen aufgelöst wurden, wird die Zustandsänderung im Speicher auf dem Bildschirm gerendert. Offensichtlich möchten Sie Ihrem Benutzer Feedback geben, während dies geschieht. Daher bietet `useTransition` eine boolesche Variable `pending`, die Sie verwenden können, um eine Art Inline-"Lade"-Benachrichtigung anzuzeigen, während Suspensionen im Speicher aufgelöst werden.
Wenn Sie darüber nachdenken, möchten Sie wahrscheinlich nicht, dass Ihre bestehende Benutzeroberfläche unbegrenzt angezeigt wird, während Ihr Ladevorgang aussteht. Wenn der Benutzer versucht, etwas zu tun, und eine lange Zeit vergeht, bevor es abgeschlossen ist, sollten Sie die bestehende Benutzeroberfläche wahrscheinlich als veraltet und ungültig betrachten. Zu diesem Zeitpunkt möchten Sie wahrscheinlich, dass Ihr Komponententeam suspendiert und Ihr <Suspense>-Fallback angezeigt wird.
Um dies zu erreichen, nimmt `useTransition` einen Wert für `timeoutMs` entgegen. Dieser gibt die Zeit an, die Sie bereit sind, die Zustandsänderung im Speicher auszuführen, bevor Sie suspendieren.
const Component = props => {
const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });
// .....
};
Hier ist `startTransition` eine Funktion. Wenn Sie eine Zustandsänderung "im Speicher" ausführen möchten, rufen Sie `startTransition` auf und übergeben eine Lambda-Funktion, die Ihre Zustandsänderung durchführt.
startTransition(() => {
dispatch({ type: LOAD_DATA_OR_SOMETHING, value: 42 });
})
Sie können `startTransition` überall aufrufen. Sie können es an untergeordnete Komponenten weitergeben, usw. Wenn Sie es aufrufen, erfolgt jede Zustandsänderung, die Sie durchführen, im Speicher. Wenn eine Suspension auftritt, wird `isPending` true, was Sie verwenden können, um eine Art Inline-Ladeanzeige anzuzeigen.
Das ist alles. Das ist, was Suspense tut.
Der Rest dieses Beitrags wird sich mit tatsächlichem Code befassen, um diese Funktionen zu nutzen.
Beispiel: Navigation
Um die Navigation in Suspense einzubinden, werden Sie sich freuen zu wissen, dass React eine primitive Funktion dafür bietet: React.lazy. Es ist eine Funktion, die eine Lambda-Funktion entgegennimmt, die ein Promise zurückgibt, welches zu einer React-Komponente aufgelöst wird. Das Ergebnis dieses Funktionsaufrufs wird Ihre lazy geladene Komponente. Das klingt kompliziert, sieht aber so aus
const SettingsComponent = lazy(() => import("./modules/settings/settings"));
SettingsComponent ist nun eine React-Komponente, die, wenn sie gerendert wird (aber nicht vorher), die von uns übergebene Funktion aufruft, die dann import() aufruft und das JavaScript-Modul unter ./modules/settings/settings lädt.
Das Wichtigste ist: Während dieser import()-Aufruf läuft, suspendiert die Komponente, die SettingsComponent rendert. Es scheint, dass wir alle Teile haben, also fügen wir sie zusammen und bauen eine Suspense-basierte Navigation.
Navigationshelfer
Aber zuerst, zur Einordnung, werde ich kurz erklären, wie der Navigationszustand in dieser App verwaltet wird, damit der Suspense-Code besser verständlich ist.
Ich werde meine Booklist-App verwenden. Es ist nur ein Nebenprojekt von mir, das ich hauptsächlich pflege, um mit bahnbrechenden Webtechnologien herumzuspielen. Es wurde von mir allein geschrieben, also erwarten Sie, dass Teile davon etwas unfertig sind (besonders das Design).
Die App ist klein, mit etwa acht verschiedenen Modulen, zu denen ein Benutzer navigieren kann, ohne tiefere Navigation. Jeder Suchzustand, den ein Modul verwenden könnte, wird in der Abfragezeichenkette der URL gespeichert. In diesem Sinne gibt es einige Methoden, die den aktuellen Modulnamen und den Suchzustand aus der URL extrahieren. Dieser Code verwendet die Pakete query-string und history von npm und sieht in etwa so aus (einige Details wurden zur Vereinfachung weggelassen, wie z. B. Authentifizierung).
import createHistory from "history/createBrowserHistory";
import queryString from "query-string";
export const history = createHistory();
export function getCurrentUrlState() {
let location = history.location;
let parsed = queryString.parse(location.search);
return {
pathname: location.pathname,
searchState: parsed
};
}
export function getCurrentModuleFromUrl() {
let location = history.location;
return location.pathname.replace(/\//g, "").toLowerCase();
}
Ich habe einen appSettings-Reducer, der die aktuellen Werte für das Modul und den searchState für die App speichert und diese Methoden verwendet, um bei Bedarf mit der URL zu synchronisieren.
Die Komponenten einer Suspense-basierten Navigation
Lassen Sie uns mit einigen Suspense-Arbeiten beginnen. Zuerst erstellen wir die lazy-geladenen Komponenten für unsere Module.
const ActivateComponent = lazy(() => import("./modules/activate/activate"));
const AuthenticateComponent = lazy(() =>
import("./modules/authenticate/authenticate")
);
const BooksComponent = lazy(() => import("./modules/books/books"));
const HomeComponent = lazy(() => import("./modules/home/home"));
const ScanComponent = lazy(() => import("./modules/scan/scan"));
const SubjectsComponent = lazy(() => import("./modules/subjects/subjects"));
const SettingsComponent = lazy(() => import("./modules/settings/settings"));
const AdminComponent = lazy(() => import("./modules/admin/admin"));
Jetzt brauchen wir eine Methode, die die richtige Komponente basierend auf dem aktuellen Modul auswählt. Wenn wir React Router verwenden würden, hätten wir einige schöne <Route />-Komponenten. Da wir dies manuell machen, reicht ein switch aus.
export const getModuleComponent = moduleToLoad => {
if (moduleToLoad == null) {
return null;
}
switch (moduleToLoad.toLowerCase()) {
case "activate":
return ActivateComponent;
case "authenticate":
return AuthenticateComponent;
case "books":
return BooksComponent;
case "home":
return HomeComponent;
case "scan":
return ScanComponent;
case "subjects":
return SubjectsComponent;
case "settings":
return SettingsComponent;
case "admin":
return AdminComponent;
}
return HomeComponent;
};
Das Ganze zusammengefügt
Nachdem die langweilige Einrichtung erledigt ist, sehen wir uns an, wie die gesamte App-Wurzel aussieht. Es gibt viel Code hier, aber ich verspreche, relativ wenige dieser Zeilen beziehen sich auf Suspense, und ich werde alles davon abdecken.
const App = () => {
const [startTransitionNewModule, isNewModulePending] = useTransition({
timeoutMs: 3000
});
const [startTransitionModuleUpdate, moduleUpdatePending] = useTransition({
timeoutMs: 3000
});
let appStatePacket = useAppState();
let [appState, _, dispatch] = appStatePacket;
let Component = getModuleComponent(appState.module);
useEffect(() => {
startTransitionNewModule(() => {
dispatch({ type: URL_SYNC });
});
}, []);
useEffect(() => {
return history.listen(location => {
if (appState.module != getCurrentModuleFromUrl()) {
startTransitionNewModule(() => {
dispatch({ type: URL_SYNC });
});
} else {
startTransitionModuleUpdate(() => {
dispatch({ type: URL_SYNC });
});
}
});
}, [appState.module]);
return (
<AppContext.Provider value={appStatePacket}>
<ModuleUpdateContext.Provider value={moduleUpdatePending}>
<div>
<MainNavigationBar />
{isNewModulePending ? <Loading /> : null}
<Suspense fallback={<LongLoading />}>
<div id="main-content" style={{ flex: 1, overflowY: "auto" }}>
{Component ? <Component updating={moduleUpdatePending} /> : null}
</div>
</Suspense>
</div>
</ModuleUpdateContext.Provider>
</AppContext.Provider>
);
};
Zuerst haben wir zwei verschiedene Aufrufe von useTransition. Wir werden einen für das Routing zu einem neuen Modul und den anderen für die Aktualisierung des Suchzustands für das aktuelle Modul verwenden. Warum der Unterschied? Nun, wenn der Suchzustand eines Moduls aktualisiert wird, möchte dieses Modul wahrscheinlich eine Inline-Ladeanzeige anzeigen. Dieser sich aktualisierende Zustand wird von der Variablen moduleUpdatePending gehalten, die Sie, wie Sie sehen werden, in den Kontext für das aktive Modul legen, um sie bei Bedarf abzurufen und zu verwenden.
<div>
<MainNavigationBar />
{isNewModulePending ? <Loading /> : null}
<Suspense fallback={<LongLoading />}>
<div id="main-content" style={{ flex: 1, overflowY: "auto" }}>
{Component ? <Component updating={moduleUpdatePending} /> : null} // highlight
</div>
</Suspense>
</div>
Das appStatePacket ist das Ergebnis des App-Zustands-Reducers, den ich oben besprochen (aber nicht gezeigt) habe. Es enthält verschiedene Teile des Anwendungszustands, die sich selten ändern (Farbthema, Offline-Status, aktuelles Modul usw.).
let appStatePacket = useAppState();
Etwas später greife ich die Komponente ab, die gerade aktiv ist, basierend auf dem aktuellen Modulnamen. Anfangs wird dies null sein.
let Component = getModuleComponent(appState.module);
Der erste Aufruf von useEffect weist unseren appSettings-Reducer an, sich beim Start mit der URL zu synchronisieren.
useEffect(() => {
startTransitionNewModule(() => {
dispatch({ type: URL_SYNC });
});
}, []);
Da dies das initiale Modul ist, zu dem die Web-App navigiert, wickle ich es in startTransitionNewModule ein, um anzuzeigen, dass ein neues Modul geladen wird. Obwohl es verlockend sein könnte, den appSettings-Reducer den Namen des initialen Moduls als seinen initialen Zustand haben zu lassen, verhindert dies, dass wir unseren startTransitionNewModule-Callback aufrufen. Das bedeutet, dass unsere Suspense-Grenze den Fallback sofort statt nach dem Timeout rendern würde.
Der nächste Aufruf von useEffect richtet ein History-Abonnement ein. Egal was passiert, wenn sich die URL ändert, weisen wir unsere App-Einstellungen an, sich gegen die URL zu synchronisieren. Der einzige Unterschied ist, welcher startTransition dieser Aufruf umschlossen ist.
useEffect(() => {
return history.listen(location => {
if (appState.module != getCurrentModuleFromUrl()) {
startTransitionNewModule(() => {
dispatch({ type: URL_SYNC });
});
} else {
startTransitionModuleUpdate(() => {
dispatch({ type: URL_SYNC });
});
}
});
}, [appState.module]);
Wenn wir zu einem neuen Modul navigieren, rufen wir startTransitionNewModule auf. Wenn wir eine Komponente laden, die noch nicht geladen wurde, wird React.lazy suspendieren, und die ausstehende Anzeige, die nur für die App-Wurzel sichtbar ist, wird gesetzt, was einen Lade-Spinner oben in der App anzeigt, während die Lazy-Komponente abgerufen und geladen wird. Aufgrund der Funktionsweise von useTransition bleibt der aktuelle Bildschirm drei Sekunden lang sichtbar. Wenn diese Zeit abläuft und die Komponente immer noch nicht bereit ist, wird unsere Benutzeroberfläche suspendieren und der Fallback wird gerendert, der die <LongLoading />-Komponente anzeigt.
{isNewModulePending ? <Loading /> : null}
<Suspense fallback={<LongLoading />}>
<div id="main-content" style={{ flex: 1, overflowY: "auto" }}>
{Component ? <Component updating={moduleUpdatePending} /> : null}
</div>
</Suspense>
Wenn wir die Module nicht wechseln, rufen wir startTransitionModuleUpdate auf.
startTransitionModuleUpdate(() => {
dispatch({ type: URL_SYNC });
});
Wenn die Aktualisierung eine Suspension verursacht, wird die ausstehende Anzeige, die wir im Kontext platzieren, ausgelöst. Die aktive Komponente kann dies erkennen und die Inline-Ladeanzeige anzeigen, die sie möchte. Wie zuvor, wenn die Suspension länger als drei Sekunden dauert, wird dieselbe Suspense-Grenze von zuvor ausgelöst... es sei denn, wie wir später sehen werden, gibt es eine Suspense-Grenze weiter unten im Baum.
Eine wichtige Sache, die man beachten sollte, ist, dass diese drei Sekunden langen Timeouts nicht nur für das Laden der Komponente gelten, sondern auch dafür, dass sie zur Anzeige bereit ist. Wenn die Komponente in zwei Sekunden geladen wird und beim Rendern im Speicher (da wir uns innerhalb eines startTransition-Aufrufs befinden) suspendiert, wartet `useTransition` weiter bis zu einer weiteren Sekunde, bevor es suspendiert.
Beim Schreiben dieses Blogbeitrags habe ich die Slow-Network-Modi von Chrome verwendet, um das Laden zu verlangsamen und meine Suspense-Grenzen zu testen. Die Einstellungen finden Sie im Tab "Netzwerk" der Chrome-Entwicklertools.

Öffnen wir unsere App im Einstellungsmodul. Dies wird aufgerufen
dispatch({ type: URL_SYNC });
Unser appSettings-Reducer synchronisiert sich mit der URL und setzt dann das Modul auf "settings". Dies geschieht innerhalb von startTransitionNewModule, sodass, wenn die lazy-geladene Komponente versucht zu rendern, sie suspendiert. Da wir uns innerhalb von startTransitionNewModule befinden, schaltet sich isNewModulePending auf true, und die <Loading />-Komponente wird gerendert.


<LongLoading />-Komponente.
Was passiert also, wenn wir woandershin navigieren? Grundsätzlich dasselbe wie zuvor, nur dass dieser Aufruf
dispatch({ type: URL_SYNC });
…vom zweiten Instanz von useEffect kommt. Gehen wir zum Bücher-Modul und sehen wir, was passiert. Zuerst wird der Inline-Spinner wie erwartet angezeigt.



Suchen und Aktualisieren
Bleiben wir im Bücher-Modul und aktualisieren wir die URL-Suchzeichenfolge, um eine neue Suche auszulösen. Denken Sie daran, dass wir in der zweiten useEffect-Funktion dasselbe Modul erkannt und eine spezielle useTransition-Aufrufung dafür verwendet haben. Von dort haben wir die ausstehende Anzeige im Kontext für das jeweils aktive Modul platziert, um sie abzurufen und zu verwenden.
Sehen wir uns den Code an, der dies tatsächlich verwendet. Hier gibt es nicht wirklich viel Suspense-bezogenen Code. Ich greife den Wert aus dem Kontext ab, und wenn er true ist, rendere ich einen Inline-Spinner über meinen vorhandenen Ergebnissen. Denken Sie daran, dass dies geschieht, wenn ein useTransition-Aufruf begonnen hat und die App im Speicher suspendiert ist. Während dies geschieht, zeigen wir weiterhin die vorhandene Benutzeroberfläche an, aber mit dieser Ladeanzeige.
const BookResults: SFC<{ books: any; uiView: any }> = ({ books, uiView }) => {
const isUpdating = useContext(ModuleUpdateContext);
return (
<>
{!books.length ? (
<div
className="alert alert-warning"
style={{ marginTop: "20px", marginRight: "5px" }}
>
No books found
</div>
) : null}
{isUpdating ? <Loading /> : null}
{uiView.isGridView ? (
<GridView books={books} />
) : uiView.isBasicList ? (
<BasicListView books={books} />
) : uiView.isCoversList ? (
<CoversView books={books} />
) : null}
</>
);
};
Legen wir einen Suchbegriff fest und sehen wir, was passiert. Zuerst wird der Inline-Spinner angezeigt.

Dann, wenn das useTransition-Timeout abläuft, erhalten wir den Fallback der Suspense-Grenze. Das Bücher-Modul definiert seine eigene Suspense-Grenze, um eine fein abgestimmtere Ladeanzeige bereitzustellen, die so aussieht

Dies ist ein wichtiger Punkt. Wenn Sie Fallbacks für Suspense-Grenzen erstellen, versuchen Sie nicht, irgendeinen Spinner und "Laden"-Nachricht anzuzeigen. Das war für unsere Top-Level-Navigation sinnvoll, weil es nicht viel anderes zu tun gab. Aber wenn Sie sich in einem bestimmten Teil Ihrer Anwendung befinden, versuchen Sie, die gleichen Komponenten wie zuvor zu verwenden, mit einer Art Ladeanzeige dort, wo die Daten wären – aber mit allem anderen deaktiviert.
Dies sind die relevanten Komponenten für mein Bücher-Modul.
const RenderModule: SFC<{}> = ({}) => {
const uiView = useBookSearchUiView();
const [lastBookResults, setLastBookResults] = useState({
totalPages: 0,
resultsCount: 0
});
return (
<div className="standard-module-container margin-bottom-lg">
<Suspense fallback={<Fallback uiView={uiView} {...lastBookResults} />}>
<MainContent uiView={uiView} setLastBookResults={setLastBookResults} />
</Suspense>
</div>
);
};
const Fallback: SFC<{
uiView: BookSearchUiView;
totalPages: number;
resultsCount: number;
}> = ({ uiView, totalPages, resultsCount }) => {
return (
<>
<BooksMenuBarDisabled
totalPages={totalPages}
resultsCount={resultsCount}
/>
{uiView.isGridView ? (
<GridViewShell />
) : (
<h1>
Books are loading <i className="fas fa-cog fa-spin"></i>
</h1>
)}
</>
);
};
Eine kurze Anmerkung zur Konsistenz
Bevor wir fortfahren, möchte ich auf eine Sache aus den früheren Screenshots hinweisen. Schauen Sie sich den Inline-Spinner an, der angezeigt wird, während die Suche aussteht, dann schauen Sie sich den Bildschirm an, wenn diese Suche suspendiert wurde, und als Nächstes die abgeschlossenen Ergebnisse.

Bemerken Sie, dass sich rechts neben dem Suchbereich eine Beschriftung "C++" befindet, mit der Option, sie aus der Suchanfrage zu entfernen? Oder besser gesagt, bemerken Sie, dass diese Beschriftung nur auf den beiden letzten Screenshots vorhanden ist? In dem Moment, in dem die URL aktualisiert wird, wird der Anwendungszustand, der diese Beschriftung steuert, aktualisiert; dieser Zustand wird jedoch anfangs nicht angezeigt. Anfangs suspendiert die Zustandsaktualisierung im Speicher (da wir `useTransition` verwendet haben), und die vorherige Benutzeroberfläche wird weiterhin angezeigt.
Dann wird der Fallback gerendert. Der Fallback rendert eine deaktivierte Version derselben Suchleiste, die den aktuellen Suchzustand anzeigt (nach Wahl). Wir haben nun unsere vorherige Benutzeroberfläche entfernt (da sie inzwischen ziemlich alt und veraltet ist) und warten auf die Suche, die in der deaktivierten Menüleiste angezeigt wird.
Dies ist die Art von Konsistenz, die Ihnen Suspense kostenlos bietet.
Sie können Ihre Zeit damit verbringen, schöne Anwendungszustände zu gestalten, und React erledigt die Arbeit, indem es herausfindet, ob Dinge bereit sind, ohne dass Sie mit Promises jonglieren müssen.
Verschachtelte Suspense-Grenzen
Nehmen wir an, unsere Top-Level-Navigation dauert so lange, bis unsere Bücherkomponente geladen ist, dass unser "Immer noch laden, Entschuldigung"-Spinner aus der Suspense-Grenze gerendert wird. Von dort lädt die Bücherkomponente und die neue Suspense-Grenze innerhalb der Bücherkomponente wird gerendert. Aber dann, während das Rendern fortgesetzt wird, wird unsere Buchsuchanfrage ausgelöst und suspendiert. Was wird passieren? Wird die Top-Level-Suspense-Grenze weiter angezeigt, bis alles bereit ist, oder wird die weiter unten liegende Suspense-Grenze in Büchern die Kontrolle übernehmen?
Die Antwort ist Letzteres. Wenn neue Suspense-Grenzen weiter unten im Baum gerendert werden, wird ihr Fallback den Fallback jeder vorherigen Suspense-Fallback, der bereits angezeigt wurde, ersetzen. Es gibt derzeit eine instabile API, um dies zu überschreiben, aber wenn Sie Ihre Fallbacks gut gestalten, ist dies wahrscheinlich das Verhalten, das Sie möchten. Sie möchten nicht, dass "Immer noch laden, Entschuldigung" einfach weiter angezeigt wird. Vielmehr möchten Sie, sobald die Bücherkomponente bereit ist, unbedingt diese Hülle mit der gezielteren Warteaufforderung anzeigen.
Was passiert nun, wenn unser Bücher-Modul geladen wird und mit dem Rendern beginnt, während der startTransition-Spinner immer noch angezeigt wird und dann suspendiert? Mit anderen Worten, stellen Sie sich vor, dass unsere startTransition einen Timeout von drei Sekunden hat, die Bücherkomponente gerendert wird, die verschachtelte Suspense-Grenze innerhalb von einer Sekunde im Komponententeam ist und die Suchanfrage suspendiert. Verstreichen die restlichen zwei Sekunden, bevor diese neue verschachtelte Suspense-Grenze den Fallback rendert, oder wird der Fallback sofort angezeigt? Die Antwort, vielleicht überraschend, ist, dass der neue Suspense-Fallback standardmäßig sofort angezeigt wird. Das liegt daran, dass es am besten ist, eine neue, gültige Benutzeroberfläche so schnell wie möglich anzuzeigen, damit der Benutzer sehen kann, dass Dinge passieren und Fortschritte machen.
Wie Daten passen hinein
Navigation ist in Ordnung, aber wie passt das Datenladen in all das hinein?
Es passt vollständig und transparent hinein. Das Laden von Daten löst Suspensionen aus, genau wie die Navigation mit React.lazy, und es greift auf dieselben useTransition- und Suspense-Grenzen zurück. Das ist das Erstaunliche an Suspense: Alle Ihre asynchronen Abhängigkeiten funktionieren nahtlos in demselben System. Das manuelle Verwalten dieser verschiedenen asynchronen Anfragen zur Gewährleistung der Konsistenz war früher ein Albtraum, weshalb es niemand tat. Web-Apps waren berüchtigt für kaskadierende Spinner, die zu unvorhersehbaren Zeiten stoppten und inkonsistente, nur teilweise abgeschlossene Benutzeroberflächen erzeugten.
OK, aber wie binden wir das Datenladen tatsächlich ein? Das Datenladen in Suspense ist paradoxerweise sowohl komplexer als auch einfacher.
Ich werde es erklären.
Wenn Sie auf Daten warten, werfen Sie ein Promise in die Komponente, die die Daten liest (oder versucht, sie zu lesen). Das Promise sollte konsistent sein, basierend auf der Datenanforderung. Vier wiederholte Anfragen für dieselbe "C++"-Suchanfrage sollten also dasselbe, identische Promise werfen. Dies impliziert eine Art Caching-Schicht zur Verwaltung all dessen. Dies werden Sie wahrscheinlich nicht selbst schreiben. Stattdessen hoffen Sie einfach und warten darauf, dass sich die von Ihnen verwendete Datenbibliothek zur Unterstützung von Suspense aktualisiert.
Dies ist in meiner micro-graphql-react-Bibliothek bereits geschehen. Anstelle der Verwendung des useQuery-Hooks verwenden Sie den useSuspenseQuery-Hook, der eine identische API hat, aber ein konsistentes Promise wirft, wenn Sie auf Daten warten.
Moment mal, was ist mit Vorab-Laden?
Ist Ihr Gehirn vom Lesen anderer Dinge über Suspense, die von Wasserfällen, Fetch-on-Render, Vorab-Laden usw. sprachen, zu Brei geworden? Machen Sie sich keine Sorgen. Hier ist, was es alles bedeutet.
Nehmen wir an, Sie laden die Bücherkomponente per Lazy-Load, die gerendert wird und dann einige Daten anfordert, was eine neue Suspense auslöst. Die Netzwerkanforderung für die Komponente und die Netzwerkanforderung für die Daten erfolgen nacheinander – in Wasserfallform.
Aber hier ist der entscheidende Punkt: Der Anwendungszustand, der zu der anfänglichen Abfrage führte, die beim Laden der Komponente ausgeführt wurde, war bereits verfügbar, als Sie mit dem Laden der Komponente begonnen haben (in diesem Fall die URL). Warum also nicht die Abfrage "starten", sobald Sie wissen, dass Sie sie benötigen? Sobald Sie zu /books navigieren, warum nicht gleich die aktuelle Suchanfrage auslösen, damit sie bereits in Bearbeitung ist, wenn die Komponente geladen wird.
Das micro-graphql-react-Modul hat tatsächlich eine preload-Methode, und ich fordere Sie dringend auf, sie zu verwenden. Das Vorab-Laden von Daten ist eine nette Performance-Optimierung, hat aber nichts mit Suspense zu tun. Klassische React-Apps könnten (und sollten) Daten vorab laden, sobald sie wissen, dass sie sie benötigen. Vue-Apps sollten Daten vorab laden, sobald sie wissen, dass sie sie benötigen. Svelte-Apps sollten... Sie verstehen den Punkt.
Das Vorab-Laden von Daten ist orthogonal zu Suspense, etwas, das Sie mit buchstäblich jedem Framework tun können. Es ist auch etwas, das wir alle bereits hätten tun sollen, obwohl es niemand sonst getan hat.
Aber im Ernst, wie lädt man vorab?
Das liegt bei Ihnen. Zumindest die Logik zur Ausführung der aktuellen Suche muss vollständig in ihr eigenes, eigenständiges Modul getrennt werden. Sie sollten buchstäblich sicherstellen, dass diese preload-Funktion in einer eigenen Datei liegt. Verlassen Sie sich nicht darauf, dass Webpack Tree-Shaking durchführt; Sie werden wahrscheinlich tiefe Traurigkeit erleben, wenn Sie das nächste Mal Ihre Bundles überprüfen.
Sie haben eine preload()-Methode in ihrem eigenen Bundle, also rufen Sie sie auf. Rufen Sie sie auf, wenn Sie wissen, dass Sie zu diesem Modul navigieren werden. Ich gehe davon aus, dass React Router eine Art API hat, um Code bei einer Navigationsänderung auszuführen. Für den oben genannten Vanilla-Routing-Code rufe ich die Methode in der Routing-Switch-Anweisung von zuvor auf. Ich habe sie der Kürze halber weggelassen, aber der Bücher-Eintrag sieht tatsächlich so aus.
switch (moduleToLoad.toLowerCase()) {
case "activate":
return ActivateComponent;
case "authenticate":
return AuthenticateComponent;
case "books":
// preload!!!
booksPreload();
return BooksComponent;
Das ist alles. Hier ist eine Live-Demo zum Ausprobieren.
Um den Suspense-Timeout-Wert, der standardmäßig 3000 ms beträgt, zu ändern, navigieren Sie zu Einstellungen und schauen Sie sich den Reiter "Misc" an. Stellen Sie einfach sicher, dass Sie die Seite nach der Änderung aktualisieren.
Zusammenfassung
Ich habe mich selten über etwas im Webentwicklungs-Ökosystem so gefreut wie über Suspense. Es ist ein unglaublich ehrgeiziges System zur Bewältigung eines der schwierigsten Probleme in der Webentwicklung: Asynchronität.
Erst als ich die Demo sah, bekam ich eine Idee. Toller Beitrag!
Ist der
useTransition-Hook für Produktionsanwendungen in Ordnung? Gibt es eine Möglichkeit, das in diesem großartigen Beitrag beschriebene Verhalten ohne die Verwendung desuseTransition-Hooks zu erreichen?Suspense selbst ist stabil. Suspense für Datenabrufe ist das Experimentelle. Suspense nur zum Anzeigen einer Ladeanzeige ist bereits in Produktion. Lesen Sie die Dokumentation hier -> https://reactjs.org/docs/react-api.html#reactsuspense
Können Sie das GitHub-Repo angeben? Ich möchte die Logik für die Navigationsleiste. Ich frage mich, wie Sie sie deaktivieren.