Mit Version 16.8 von React hat ein Feature Einzug gehalten, welches die Art und Weise, wie man mit React Komponenten erstellt, fundamental verändert hat: React Hooks. In diesem Artikel wollen wir uns anschauen, was Hooks sind und was sie so spannend macht.
In React gibt es im Prinzip zwei Wege, um Komponenten zu bauen: Klassenkomponenten und Funktionskomponenten. Klassenkomponenten leiten sich von React.Component ab und überschreiben die render-Methode. Diese Methode liefert eine DOM-Beschreibung der Komponente zurück und wird immer dann aufgerufen, wenn sich am Zustand der Komponente etwas geändert hat. Von außen können beliebige Daten in die Komponente hereingereicht werden, die innerhalb der Komponente mittels this.props
erreichbar sind.
Die zweite Variante sind Funktionskomponenten, die lediglich aus einer Funktion bestehen, die ähnlich wie die render
-Methode funktioniert. Der Unterschied ist, dass hier die „Props“ direkt als Parameter der Funktion übergeben werden. Von außen lässt sich bei der Benutzung einer React-Komponente nicht erkennen, ob diese als Klassen- oder Funktionskomponente implementiert wurde.
React selbst ist geprägt von Konzepten der „Funktionalen Programmierung“ und auch der Community merkt man eine Affinität zu diesem Paradigma an. Daher verwundert es nicht, dass sich besonders die Funktionskomponenten einer besonderen Beliebtheit erfreuen. Allerdings waren Funktionskomponenten bisher in ihren Möglichkeiten beschränkt, und für viele Dinge waren stets Klassenkomponenten notwendig.
Zum einen ermöglichten nur Klassenkomponenten die Verwaltung eines lokalen Zustands, während Funktionskomponenten nur die von außen hereingereichten Daten zur Anzeige bringen konnten. Außerdem haben nur Klassenkomponenten Zugriff auf die zahlreichen Lifecycle-Methoden, um beispielsweise Code auszuführen, sobald die Komponente in den Komponenten-Baum eingehangen wurde. Diese Lifecycles sind aber notwendig, um Seiteneffekte wie Netzwerk-Requests oder Timer sauber benutzen zu können.
Dies führte in der React-Community zu einer häufig anzutreffenden Unterscheidung zwischen „dummen“ Anzeige-Komponenten, die lediglich Daten entgegennehmen und hübsch darstellen, und „smarten“ Komponenten, die selber wenig zur Darstellung beitragen, aber die Daten verwalten und Logik implementieren. Auch mit Hooks gibt es gute Gründe, diese konzeptionelle Trennung beizubehalten, jedoch war bisher eben auch die Art der Implementierung notwendigerweise unterschiedlich.
Mit Hooks gibt es nun die Möglichkeit, fast alle Aspekte, für die bisher Klassenkomponenten notwendig waren, auch mit Funktionskomponenten umzusetzen. Am besten lässt sich dies am Beispiel des lokalen Zustands verdeutlichen. In Abbildung 1 ist eine Komponente zu sehen, die einen Titel und einen Text anzeigt, wobei der Text mittels eines Buttons ein- und ausgeklappt werden kann. Der dazugehörige Code, implementiert als Klassenkomponente, findet sich in Code-Block 1. Als Sprache wurde (wie auch für alle folgenden Beispiele) TypeScript gewählt.
Interessant ist vor allem die switchExpansion
-Methode. Hier wird die React-Methode setState
aufgerufen, um den lokalen Zustand des expanded
-Flag umzuschalten. In Abhängigkeit dieses Flags wird in der render
-Methode die Content-Section gerendert sowie eine CSS-Klasse gesetzt.
State Hook
Die Variante als Funktionskomponente mit Hooks ist im Code-Block 2 zu sehen. Spannend ist hier zunächst, dass die Zeile X in der useState
aufgerufen wird. Dies ist der erste React-Hook. Hooks sind spezielle Funktionen, deren Parameter und Rückgabe-Werte variieren können. Im Fall von useState liefert die Funktion ein Array mit zwei Elementen zurück: Das erste ist der aktuelle Wert der Zustandsvariable. Dieser kann im Return-Block genutzt werden, um die Darstellung zu beeinflussen. Genau wie bei Klassenkomponenten darf der Zustand nicht direkt mittels Zuweisung verändert werden, sondern es muss eine spezielle Funktion aufgerufen werden (bei Klassenkomponenten setState
), damit React von dieser Änderung Notiz nehmen und ein Neuzeichnen der Komponente veranlassen kann. Der useState-Hook liefert eine solche Update-Funktion als zweites Array-Element zurück. Nicht verwirren lassen sollte man sich von den eckigen Klammern auf der linken Seite des Gleichheitszeichens. Dies ist das so genannte „destructuring“, welches TypeScript und modernes JavaScript mitbringt, um direkt auf Array-Elemente zuzugreifen und diese lokalen Variablen zuzuweisen.
Bei Funktionskomponenten gibt es keine separate render
-Methode. Stattdessen wird die gesamte Funktion bei Bedarf neu ausgeführt. Dies ist beispielsweise der Fall, wenn sich die von außen hinein gereichten Props ändern, oder aber, wenn die von useState
als zurückgegebene Update-Funktion aufgerufen wurde. In diesem Fall liefert useState
den aktualisierten Wert als erstes Array-Element, so dass innerhalb der Komponente entsprechend darauf reagiert werden kann.
Alle mit React von Hause aus mitgelieferte Hooks folgen der Namenskonvention „useX“, und diese Konvention wird auch für Custom-Hooks dringend empfohlen, um Hooks auf den ersten Blick als solche erkennen zu können. Dies ist wichtig, da für Hooks besondere Regeln existieren, die unbedingt eingehalten werden müssen:
Hooks dürfen nur in Funktionskomponenten oder Custom-Hooks, nicht jedoch in Klassenkomponenten oder sonstigen Funktionen genutzt werden. Außerdem dürfen Hooks nicht innerhalb von IF-Verzweigungen, Loops oder Unter-Funktionen aufgerufen werden, sondern müssen auf der obersten Ebene der Funktionskomponente bzw. des Custom-Hooks stehen. Der Grund für diese Regel ist, dass React die Reihenfolge der Hook-Aufrufe verfolgt und nur so Hooks über mehrere Rendering-Zyklen zuordnen kann. In der Praxis erweisen sich diese Regeln aber als unproblematisch und man gewöhnt sich recht schnell daran. Als gute Praxis hat es sich darüber hinaus erwiesen, Hooks stets am Anfang der Funktionskomponente aufzurufen, um einen guten Überblick zu behalten.
Effect Hook
Der zweite wichtige Hook, den React mitbringt, ist useEffect. Er kommt vor allem in solchen Situationen zum Einsatz, die bei Klassenkomponenten mithilfe der Lifecycle-Methoden implementiert wurden. Ein Beispiel ist das asynchrone Laden von Daten über das Netzwerk, sobald die Komponente angezeigt wird: Bei Klassenkomponenten wird dazu die Methode componentDidMount
implementiert, welche, wie der Name schon sagt, aufgerufen wird, direkt nachdem die Komponente in den Komponenten-Baum eingefügt wurde. In dieser Methode kann der Netzwerk-Request gestartet und auf das Ergebnis subscribed werden. Sobald das Ergebnis da ist, wird mittels setState
der lokale Zustand aktualisiert, was wiederum ein Neuzeichnen der Komponente mit den geladenen Daten auslöst.
Im Code-Block 3 ist das schematisch dargestellt. Um das gleiche Ergebnis mit Funktionskomponenten zu erreichen, wird useEffect eingesetzt. Dieser Hook nimmt als erstes Argument eine Funktion entgegen, die den auszuführenden Effekt darstellt. In unserem Beispiel würden wir genau hier unseren Netzwerk-Request starten und mittels Callback auf das Ergebnis des Requests lauschen. Den Callback nutzen wir ebenfalls dazu, den lokalen State zu aktualisieren. Da wir in einer Funktionskomponente sind, ist dieser lokale State natürlich mittels useState realisiert. Der Code-Block 4 zeigt die Netzwerk-Request-Variante mit Hooks.
Wann und wie oft die Effekt-Funktion ausgeführt wird, hängt vom zweiten Argument, welche useEffect übergeben wird, ab. Dies ist ein Array von Variablen, von denen die Effekt-Funktion abhängig ist. React beobachtet die Werte im Array und führt die Effekt-Funktion nur aus, wenn sich mindestens einer davon seit dem letzten Mal geändert hat. Üblicherweise handelt es sich bei den Variablen entweder um Props oder State-Variablen von useState-Hooks. Wenn beispielsweise der Netzwerk-Request von einem Eingabeparameter des Nutzers abhängt, sollte dieser Parameter im Array von useEffect auftauchen.
Dabei gibt es zwei Sonderfälle: Wird ein leeres Array übergeben, wird die Effekt-Funktion einmalig nach dem Einhängen der Komponente ausgeführt, vergleichbar mit componentDidMount. Wird der zweite Parameter dagegen komplett weggelassen, führt React die Effekt-Funktion bei jedem Rendering erneut aus.
Zurück zum Beispiel: Aktuell steckt in beiden Code-Beispielen noch ein Bug: Wird die Komponente aus dem Komponenten-Baum entfernt, bevor der Netzwerk-Request abgeschlossen ist (beispielsweise, weil der Nutzer sofort irgendwo anders hin navigiert), dann existiert die Komponenten-Instanz möglicherweise nicht mehr, wenn der Callback ausgeführt wird. Als Ergebnis erhalten wir Null-Pointer-Exceptions. Um dies bei der Klassen-Variante zu lösen, wird ein weiterer Lifecycle namens componentWillUnmount benötigt, in dem alle Aufräumarbeiten stattfinden können.
Bei der Hooks-Variante ist diese Situation bereits direkt im useEffect-Hook vorgesehen: Die Effekt-Funktion kann ihrerseits wiederum eine Funktion zurückgeben, die von React einmalig ausgeführt wird, sobald die Komponente entfernt wird. Der Code dazu ist im Code-Block 5 zu sehen.
Man erkennt hieraus bereits einen Unterschied von useState im Vergleich zu den Lifecycle-Methoden bei Klassenkomponenten: Lifecycles folgen einem Gedankenmodell, welches sich stark auf die Komponente selbst bezieht, während der useEffect-Hook an einem häufig anzutreffenden Usecase ausgerichtet ist, nämlich das Ausführen eines bestimmten Seiteneffekts in Abhängigkeit von bestimmten Variablen. Um dies mit Lifecycles sauber zu implementieren, sind meist mehrere solcher Lifecycle-Methoden notwendig: Hängt der Request vom lokalen Zustand oder von Props ab, muss zusätzlich zu componentDidMount
auch noch componentDidUpdate
implementiert werden, sowie für Aufräumarbeiten componentWillUnmount
. Während bei useEffect die gesamte Logik, inklusive Aufräumarbeiten, kompakt an einer einzigen Stelle zusammengefasst und bei Bedarf sogar wiederverwendbar als Custom-Hook ausgelagert werden kann, ist die Logik bei Klassenmethoden über mehrere Methoden verstreut.
Eigene Hooks bauen
Neben useState und useEffect bringt React noch eine Reihe weiterer Hooks mit, die für verschiedene Anwendungsfälle gedacht sind, beispielsweise useRef, um sich Zugriff auf DOM-Elemente zu verschaffen, oder useContext, um die Context-API von React zu nutzen. Spannend ist aber auch die Möglichkeit, auf Basis der existierenden Hooks seine eigenen Custom-Hooks zu bauen. Auf diese Weise können häufig verwendete Funktionalitäten gekapselt und zur Wiederverwendung bereitgestellt werden.
Viele Dritt-Bibliotheken aus der React-Community wie Redux oder die GraphQL-Library Apollo-Client bieten bereits Varianten mit Hooks an. Daneben existieren auch Sammlungen von nützlichen Hooks im Internet, an denen man sich bedienen oder inspirieren lassen kann.
Am Beispiel eines „useMedia“-Hooks wollen wir uns anschauen, wie man eigene Hooks entwickeln kann. Der useMedia-Hook soll einen Zugriff auf MediaQuery-Abfragen bieten, die sonst vor allem beim Styling mit CSS eine Rolle spielen. Damit lässt sich zum Beispiel prüfen, ob eine bestimmte Mindest-Breite unterschritten ist, um daraufhin das Layout für mobile Geräte anzupassen. Ein anderes Beispiel ist das Abfragen, ob der Anwender ein dunkles Farbschema bevorzugt. Die Query zur Abfrage des Darkmodes lautet (prefers-color-scheme: dark
), für die Mindestbreite wird z.B. (min-width: 600px
) genutzt.
Unser Custom-Hook soll eine solche Abfrage als String entgegennehmen und als Ergebnis einen Boolean-Wert liefern, der angibt, ob die Abfrage zutrifft oder nicht. Die Browser-API für MediaQueries erlaubt nicht nur die Abfrage des aktuellen Zustands, sondern auch das Hinterlegen eines Listeners, der bei Änderung des Zustands feuert. Daher soll unser Hook ebenfalls dafür sorgen, dass bei einer Änderung des Abfrage-Ergebnisses die Komponente mit dem aktualisierten Wert neu gezeichnet wird. Dadurch können wir beispielsweise direkt darauf reagieren, wenn die Nutzer am Browser-Fenster ziehen und eine bestimmte Schwelle unter- oder überschreiten.
Der Code dazu, sowie ein Anwendungsbeispiel, ist in Code-Block 6 zu sehen. Zunächst benötigen wir useState, um den Zustand (matched die Query oder nicht?) abzubilden. Die Abfrage des Queries führen wir innerhalb von useEffect durch. Die Browser-API window.matchMedia
liefert ein Query-Objekt, welches wir zunächst nutzen, um den aktuellen Zustand in unsere lokale Zustandsvariable zu übertragen. Anschließend hinterlegen wir einen Listener auf dem Query-Objekt, der bei Änderungen des Query-Ergebnisses ausgeführt wird. In diesem Fall aktualisieren wir ebenfalls unsere lokale Zustandsvariable.
Zum Schluss geben wir eine Aufräum-Funktion an useEffect zurück, die dafür sorgt, dass der Listener wieder korrekt abgebaut wird.
Indem wir den Eingabe-String des Nutzers als Abhängigkeit zum zweiten Parameter von useEffect hinzufügen, sorgen wir außerdem dafür, dass die MediaQuery-Abfrage erneut ausgeführt wird, sollte sich die Abfrage des Nutzers ändern.
Fazit
React-Hooks zählen sicherlich zu den tiefgreifendsten Erweiterungen, die React in den letzten Jahren erfahren hat. Nicht nur, weil damit Funktionskomponenten massiv aufgewertet wurden, sondern auch, weil mit Hooks viele Aufgaben wesentlich angenehmer, kompakter und besser verständlich umgesetzt werden können. Daher ist es nicht verwunderlich, dass die Community mit Freuden auf das neue Feature aufgesprungen ist.
Nicht nur stellen viele Dritt-Bibliotheken ihre Funktionalität, da wo es Sinn ergibt, nun auch als Hooks zur Verfügung. Hooks inspirieren Library-AutorInnen auch dazu, ganz neue Wege zu bestreiten und Bibliotheken zu entwickeln, die ohne Hooks wohl kaum denkbar wären. Als Beispiel dafür sei die State-Management-Bibliothek Recoil genannt, die auf der React Europe 2020 Konferenz vorgestellte wurde.
Spannend ist darüber hinaus, dass Hooks auch über die Grenzen der React-Community hinaus EntwicklerInnen inspirieren. So implementiert die Bibliothek Haunted die Hooks-API für Web Components und erlaubt damit das Erstellen von Custom-Components nach ähnlichen Mustern wie bei React-Funktionskomponenten. Auch die Vue.js-Entwickler arbeiten an einer API-Variante, die stark von React-Hooks inspiriert ist.
Für EntwicklerInnen von React-Anwendungen stellt sich abschließend die Frage, ob die eigene Code-Basis nun gänzlich von Klassenkomponenten befreit und alles auf Hooks umgebaut werden sollte. Auch wenn Funktionskomponenten und Hooks einige Vorteile haben, bleibt auch die Klassen-API weiterhin fester Bestandteil von React, weshalb es keinen wirklichen Grund gibt, bestehende und funktionierende Komponenten umzubauen. Bei der Entwicklung von neuen Komponenten dürfte es aber eine gute Idee sein, sich eher auf Funktionskomponenten zu konzentrieren.
Allerdings gibt es auch einige Usecases, für die bisher noch keine Hooks-Alternative zu Klassenkomponenten existiert. Dazu zählt zum Beispiel die Möglichkeit, sogenannte Error-Boundaries zu implementieren, die dafür sorgen, dass bei einem Fehler in einer Komponente nicht gleich die gesamte Anwendung abschmiert. Für die dafür notwendigen Lifecycle-Methoden getDerivedStateFromError
und componentDidCatch
existieren zum gegenwärtigen Zeitpunkt noch keine Hooks-Alternativen. Langfristig dürften aber auch diese Lücken geschlossen werden, so dass die Zukunft von React deutlich in Richtung Funktionskomponenten mit Hooks zeigt.+
- React Hooks - 9. Dezember 2020
- Static Site Generatoren: Webseiten bauen mit Gatsby - 4. Dezember 2019