Startseite / Artikel

Verdient dieser Hook seinen Platz? Zehn Überprüfungsfragen zum React-Code

Erlernen Sie zehn konkrete Überprüfungsfragen, die in React Hook-Code überflüssige Zustände, veraltete Effekte, Render-Läufe, unnötige Memoisierung sowie zu große Komponenten aufdecken.

Verlag: REACTAPP.TOP6 Min. Lesezeit1324 Wörter

Quellen: Hooks

Die meisten Fehler mit React Hooks sehen in Pull-Requests nicht wie echte Fehler aus. Eine fehlende Abhängigkeit, ein Effect, der Props in State kopiert, oder eine useCallback-Umhüllung um einen Handler, der nicht gememorisert wird – all das wird bei der Überprüfung übersehen und tritt später als veraltete Ansichten, zusätzliche Renderungen oder angesammelte Listener auf. Im Folgenden werden zehn häufige Hook-Fehler in Überprüfungsfragen umgewandelt, die Sie für jede Komponente stellen können, zusammen mit dem zugrunde liegenden Mechanismus sowie der einfacheren Form, die der Code in der Regel benötigt.

Fragen zum State

Kann dieser Wert während des Renderings berechnet werden?

Ein gängiges Muster besteht darin, fullName im State zu speichern und einen Effect zu verwenden, um setFullName(`${firstName} ${lastName}`) aufzurufen, sobald sich einer der Namen ändert. Die Komponente wird einmal mit dem alten Wert gerendert, der Effect läuft nach dem Zeichnen, und der Setter plant eine zweite Renderung. Für einen Frame ist die UI inkonsistent, und es muss ein zusätzlicher Bestandteil verwaltet werden.

Berechnen Sie es stattdessen so: const fullName = `${firstName} ${lastName}`;. React führt die Komponente bei jeder Renderung erneut aus, sodass der Wert stets mit den Eingaben synchron ist. Wenn etwas aus Props oder State abgeleitet werden kann, handelt es sich nicht um State. Siehe auch warum das Synchronisieren von State mit useEffect kontraproduktiv ist.

Hält die Komponente mehr State bereit, als sie benötigt?

Derselbe Fehler in größerem Maßstab: Ein Formular mit drei useState-Aufrufen für firstName, lastName und fullName. Jeder Namewechsel muss nun auch in fullName widergespiegelt werden, und jeder Pfad, der das vergisst, verursacht einen Fehler. Behalten Sie die beiden unabhängigen Eingabefelder bei und erzeugen Sie das dritte daraus. Für jeden useState prüfen Sie, ob der Wert aus den anderen wiederaufgebaut werden kann; falls ja, löschen Sie ihn.

Muss ein Änderung dieses Wertes das Display aktualisieren?

Der State existiert dazu, die Darstellung zu steuern. Ein Timer-Id, ein DOM-Node, ein vorheriger Prop-Wert oder eine Instanz von Drittanbietern (ein Map, ein Chart, ein Socket-Client) beeinflusst in der Regel nicht das, was dargestellt wird. Daher bedeutet useState(null) in diesem Fall, dass jede Zuweisung eine sinnlose Neudarstellung auslöst. Verwenden Sie stattdessen const timerRef = useRef(null);: Durch das Schreiben von timerRef.current bleibt der Wert über mehrere Neudarstellungen hinweg erhalten, ohne dass eine weitere ausgelöst wird. Die Kehrseite ist, dass man niemals einen Ref lesen sollte, um zu entscheiden, was angezeigt werden soll. Wenn die Benutzeroberfläche den Wert widerspiegeln muss, handelt es sich schließlich um State.

Fragen zu Effects

Enthält die Abhängigkeitsliste alles, was der Effect liest?

Durch das Übergeben von [] wird ein Effekt so konfiguriert, dass er „nur einmal ausgeführt“ wird, wie in useEffect(() => { fetchUser(userId); }, []);. Dadurch wird der Wert von userId bereits bei der ersten Darstellung festgelegt. Wenn sich die Eigenschaft ändert, wird nichts neu abgerufen und die Benutzeroberfläche zeigt den falschen Benutzer an. Durch das Verwenden von [userId] wird dieses Problem behoben.

Der Array dient nicht dazu, den Ausführungstakt zu steuern; er legt lediglich fest, welche Werte der Effekt abruft. Wenn der Effekt zu oft ausgeführt wird, sollte man stattdessen ändern, welche Werte er abruft (z. B. eine Funktion darin platzieren oder sich auf ein Primitiv statt auf ein Objekt verlassen), anstatt Abhängigkeiten zu verbergen. Die Lint-Regel react-hooks/exhaustive-deps erfasst fast alle solchen Fälle, weshalb deren Warnungen als Fehler behandelt werden sollten. Effekte, die Daten abrufen, sollten außerdem bei schnellen Änderungen von userId veraltete Anfragen in ihrem Cleanup-Code ignorieren oder abbrechen.

Kann dieser Effekt sich selbst auslösen?

useEffect(() => { setCount(count + 1); }, [count]); ist eine Schleife: Die Aktualisierung ändert count, wodurch der Effekt endlos erneut ausgeführt wird. Der funktionale Aktualisierer setCount(prev => prev + 1) liest den neuesten Wert, ohne count in der Schließung benötigen zu müssen, sodass die Abhängigkeit entfernt werden kann und der Effekt nur beim Erstladen ausgeführt wird. Beachten Sie, dass er dennoch bei jedem Erstladen um eins erhöht wird, und da im Entwicklungsmodus der strenge Modus zweimal lädt, kann es lokal zu einem Sprung um zwei kommen.

Die bessere Frage ist oft, ob die Aktualisierung überhaupt in einen Effekt gehören sollte. Reaktionen auf Benutzeraktionen gehen in Event-Handler; abgeleitete Werte werden während des Renderings berechnet. Effekte dienen dazu, mit Systemen außerhalb von React zu synchronisieren.

Löscht der Effekt das, was er einrichtet?

Ein Effekt, der nur window.addEventListener('resize', handleResize) aufruft, fügt bei jedem Laden einen neuen Zuhörer hinzu und entfernt ihn niemals. Geben Sie eine Aufräumfunktion zurück, die window.removeEventListener mit dem gleichen Handler aufruft. React ruft sie vor erneutem Ausführen des Effekts sowie beim Entladen auf. Ohne sie entstehen Speicherverluste, die Handler werden mehrfach ausgelöst und es kommen Updates an bereits nicht mehr vorhandene Komponenten.

Eine schnelle Überprüfung: Jeder add, connect oder setInterval in einem Effekt benötigt einen entsprechenden remove, disconnect oder clearInterval in seinem Rückgabewert. Die zehn Fehlerarten bei useEffect-Audits behandeln weitere mögliche Ausfallmuster von Effekten.

Fragen zur Memoisierung

Ist diese Berechnung tatsächlich aufwendig?

Die Umhüllung einer Zeichenkettenkombination mit useMemo und [user] als Abhängigkeit kostet mehr, als sie einspart. React weist weiterhin den Callback sowie das Array zu und vergleicht die Abhängigkeiten bei jeder Renderung – was mehr Arbeit bedeutet als das Zusammenfügen zweier Zeichenketten. Der Code wird außerdem schwieriger zu lesen, und falls user bei jeder Renderung ein neues Objekt ist, wird die Memoisierung dennoch erneut berechnet.

Nutzen Sie einfach die direkte Ausdrucksform. Wenden Sie useMemo nur bei deutlich aufwendigeren Aufgaben an, wie zum Beispiel dem Sortieren oder Filtern einer großen Liste, oder wenn ein gememoisierter Kindkomponente oder ein anderer Hook eine stabile Objektreferenz benötigt. Messen Sie zunächst mit dem React DevTools Profiler.

Hängt etwas von der Identität dieser Funktion ab?

useCallback sorgt dafür, dass eine Funktionsreferenz stabil bleibt – was nur dann von Bedeutung ist, wenn etwas Referenzen vergleicht: ein Kindelement, das in React.memo eingebettet ist, oder die Abhängigkeitsliste eines anderen Hooks. Das Einbetten eines Klick-Handlers, der lediglich etwas ausgibt und an einen gewöhnlichen button gebunden ist, bringt keinen Vorteil, da dem DOM egal ist, ob es sich um eine neue Funktion handelt. Schreiben Sie einfach eine normale Funktion und fügen Sie useCallback hinzu, wenn Sie den Verbraucher nennen können, der sie benötigt. Der React Compiler kann eine solche Memoisierung automatisch anwenden; überprüfen Sie die aktuellen Dokumente, um den Status in Ihrer Konfiguration zu erfahren.

Fragen zur Struktur

Wird jeder Hook bedingungslos und in derselben Reihenfolge aufgerufen?

Die Deklaration von useState ausschließlich innerhalb von if (isLoggedIn) verstößt gegen die Regeln der Hooks. React vergleicht Hooks anhand ihrer Aufrufposition in einer pro-Komponente erstellten Liste, nicht nach Namen. Wenn ein Aufruf ausgelassen wird, liest jeder folgende Hook den Slot seines Nachbarn und erzeugt somit falsche Werte oder einen Laufzeitfehler wegen einer veränderten Anzahl an Hooks.

Rufen Sie alle Hooks auf der obersten Ebene auf und gehen Sie anschließend in eine Unterabfrage über: Deklarieren Sie zunächst den user-Zustand und geben Sie danach null zurück, falls der Benutzer nicht angemeldet ist. Dasselbe gilt für Schleifen und vorzeitige Rückgänge. Zu den internen Abläufen siehe warum die Regeln der Hooks existieren.

Erledigt diese Komponente zu viele Aufgaben?

Eine Komponente, die Hunderte von Zeilen an Lade-, Zustands- und Geschäftslogik mit Markup vermischt, ist schwer zu lesen, zu testen und zu ändern. Extrahieren Sie die Logik in einen benutzerdefinierten Hook. Ein useUserData(userId)-Hook verwaltet den user-Zustand sowie den Effekt, der fetchUser(userId).then(setUser) aufruft, sobald sich userId ändert, und gibt anschließend user zurück. UserProfile reduziert sich auf const user = useUserData(userId); zusammen mit der Anzeige von user?.name.

Dadurch erhalten Sie:

  • Möglichkeit zur Wiederverwendung ohne Kopieren der Logik zwischen Komponenten.
  • Tests für die Logik, die unabhängig vom Markup sind.
  • Kürzere Komponenten, die leicht zu durchforsten sind.
  • Einen einzigen Ort, an dem Lade- oder Caching-Logik geändert werden kann.

Vergessen Sie nicht, dass jeder Aufrufer eines benutzerdefinierten Hooks seinen eigenen Zustand erhält: Es wird Logik geteilt, nicht Daten.

Haupterkenntnisse

  • Ableitete Werte sind Ausdrücke, keine Zustände und keine Effekte.
  • Halten Sie den Zustand minimal; Werte, die niemals auf dem Bildschirm angezeigt werden, gehen in refs.
  • Abhängigkeitsarrays beschreiben, was ein Effekt liest – überprüfen Sie sie daher sorgfältig.
  • Jeder Effekt, der etwas einrichtet, muss es wieder rückgängig machen.
  • Memoisieren Sie nur dann, wenn eine Messung oder ein referenzsensibler Verbraucher dies rechtfertigt.
  • Rufen Sie Hooks bedingungslos auf und verlagern Sie große Logikblöcke in benannte Hooks.

Die meisten dieser Probleme entstehen dadurch, dass man aus Gewohnheit auf einen Hook zurückgreift. Die Frage „Brauchen wir diesen Hook hier wirklich?“ während der Überprüfung ist unkompliziert und spart lange Debugging-Sitzungen.