Ich musste kürzlich ein Widget in React erstellen, das Daten von mehreren API-Endpunkten abruft. Wenn der Benutzer herumklickt, werden neue Daten abgerufen und in die Benutzeroberfläche eingepasst. Aber das verursachte einige Probleme.
Ein Problem wurde schnell deutlich: Wenn der Benutzer schnell genug herumklickte, wurden bei der Auflösung früherer Netzwerkanfragen die Benutzeroberfläche kurzzeitig mit falschen, veralteten Daten aktualisiert.
Wir können unsere UI-Interaktionen debouncen, aber das löst unser Problem grundsätzlich nicht. Veraltete Netzwerkanfragen werden aufgelöst und aktualisieren unsere Benutzeroberfläche mit falschen Daten, bis die letzte Netzwerkanfrage abgeschlossen ist und unsere Benutzeroberfläche mit dem endgültigen korrekten Zustand aktualisiert. Das Problem wird bei langsameren Verbindungen deutlicher. Außerdem bleiben wir mit nutzlosen Netzwerkanfragen zurück, die die Daten des Benutzers verschwenden.
Hier ist ein Beispiel, das ich zur Veranschaulichung des Problems erstellt habe. Es holt Spieleangebote von Steam über die coole Cheap Shark API unter Verwendung der modernen `fetch()`-Methode. Versuchen Sie, den Preisgrenzwert schnell zu ändern, und Sie werden sehen, wie die Benutzeroberfläche mit falschen Daten aufblitzt, bis sie sich endlich stabilisiert.
Die Lösung
Es stellt sich heraus, dass es eine Möglichkeit gibt, ausstehende asynchrone DOM-Anfragen mit einem `AbortController` abzubrechen. Sie können ihn verwenden, um nicht nur HTTP-Anfragen, sondern auch Event-Listener abzubrechen.
Die **`AbortController`**-Schnittstelle repräsentiert ein Controller-Objekt, das es Ihnen ermöglicht, eine oder mehrere Webanfragen nach Belieben abzubrechen.
—Mozilla Developer Network
Die `AbortController`-API ist einfach: Sie stellt ein `AbortSignal` zur Verfügung, das wir in unsere `fetch()`-Aufrufe einfügen, so
const abortController = new AbortController()
const signal = abortController.signal
fetch(url, { signal })
Von nun an können wir `abortController.abort()` aufrufen, um sicherzustellen, dass unser ausstehender Fetch abgebrochen wird.
Schreiben wir unser Beispiel neu, um sicherzustellen, dass wir alle ausstehenden Fetches abbrechen und nur die neuesten vom API erhaltenen Daten in unsere App einpassen.
Der Code ist weitgehend derselbe mit wenigen wichtigen Unterschieden
- Es wird eine neue gecachte Variable `abortController` in einem `useRef` in der Komponente `
` erstellt. - Für jeden neuen Fetch wird dieser Fetch mit einem neuen `AbortController` initialisiert und sein entsprechendes `AbortSignal` abgerufen.
- Das abgerufene `AbortSignal` wird an den `fetch()`-Aufruf übergeben.
- Es bricht sich beim nächsten Fetch selbst ab.
const App = () => {
// Same as before, local variable and state declaration
// ...
// Create a new cached variable abortController in a useRef() hook
const abortController = React.useRef()
React.useEffect(() => {
// If there is a pending fetch request with associated AbortController, abort
if (abortController.current) {
abortController.abort()
}
// Assign a new AbortController for the latest fetch to our useRef variable
abortController.current = new AbortController()
const { signal } = abortController.current
// Same as before
fetch(url, { signal }).then(res => {
// Rest of our fetching logic, same as before
})
}, [
abortController,
sortByString,
upperPrice,
lowerPrice,
])
}
Fazit
Das ist alles! Wir haben jetzt das Beste aus beiden Welten: Wir debouncen unsere UI-Interaktionen **und** wir brechen manuell veraltete ausstehende Netzwerkanfragen ab. Auf diese Weise stellen wir sicher, dass unsere Benutzeroberfläche einmal und nur mit den neuesten Daten von unserem API aktualisiert wird.
Ersteller von CheapShark hier, danke, dass Sie die API in Ihrem Artikel verwendet haben! Ich benutze CSS Tricks seit etwa 10 Jahren, daher war es ziemlich cool, meine Seite als Beispiel für die API-Nutzung zu sehen! :)
Hallo! Das ist ja cool! Es freut mich, dass es Ihnen gefallen hat, und danke, dass Sie diese großartige öffentliche API bereitstellen!
Ich glaube, Sie haben `.current` im Aufruf von `.abort()` im Beispiel vergessen. Refs sind in dieser Hinsicht etwas mühsam. (Ich vermisse `this`!)
Hallo! Wenn Sie sich die zweite Demo ansehen, bevor ich den `fetch`-Befehl ausgebe, verwende ich tatsächlich die Eigenschaft `.current` wie folgt:
abortController.current.abort()Bitte lassen Sie mich wissen, ob Sie etwas anderes im Sinn hatten? Danke für Ihr Feedback!
Oh, ich meinte das letzte Codebeispiel vor der Zusammenfassung. Das Codepen ist darüber und ist bereits in Ordnung.
Toller Tipp… Aber wenn man noch einen Schritt weiter geht, wie wäre es, eine Cleanup-Funktion aus Ihrem `useEffect`-Hook zurückzugeben, die auch alle API-Aufrufe abbricht, die gerade noch ausgeführt werden, wenn die Komponente unmounted wird?
Vielleicht bin ich eigenartig, aber ich habe nichts dagegen, dass mehrere Updates als Reaktion auf schnelles Klicken erfolgen. Ich sehe es als Indikator dafür, dass es auf jeden Klick reagiert und korrekt funktioniert.
Wenn es nur einmal aktualisiert, könnte ich misstrauisch sein, dass es nur auf den ersten Klick reagiert hat (das ist ein Ergebnis von Erfahrungen mit Benutzeroberflächen mit schlechter UX).
Das Problem ist, dass Antworten möglicherweise nicht in der gleichen Reihenfolge ankommen, wie die Anfragen gestellt wurden. Wenn jede Antwort die vorherigen Daten überschreibt, kann dies zu Fehlern führen.
Ein häufiges Beispiel sind Suchanfragen, die mehrmals pro Sekunde ausgelöst werden können, während der Benutzer tippt. Wenn die vorherige Suchanfrage nicht abgebrochen wird, kann diese Antwort nach den neueren Ergebnissen eintreffen. Dies führt zu einem ärgerlichen Fehler, bei dem die falschen Suchergebnisse angezeigt werden.
Es ist auch eine Verschwendung von Rechenleistung, eine veraltete Suchantwort zu verarbeiten, wenn bereits eine neue Anfrage läuft.
Sie werden durch abgebrochene Anfragen nicht verwirrt. Sie werden in der Netzwerkanzeige normal aufgeführt, nur mit dem Status "abgebrochen".
Die Verwendung eines Refs ist hier "zu viel". Nicht nötig hier. Ein besserer Weg ist die Verwendung der Rückruffunktion von useEffect.
Erstellen Sie jedes Mal einen neuen Controller, wenn Ihr Effekt aufgerufen wird, und geben Sie `() => abortController.abort()` zurück.
Hallo, das ist absolut ein valider Punkt! Ich sehe keine Nachteile darin.