© Excellent backgrounds/Shutterstock.com
Das neue Hooks API von React im Überblick

Captain Hooks


Die sogenannten „Hooks“ sind bei React eine der spannendsten Neuerungen der letzten Zeit. Sie ermöglichen es, auch in Funktionskomponenten alltägliche Dinge wie lokalen Zustand zu managen und Seiteneffekte durchzuführen, was vorher nur mit Klassenkomponenten möglich war. Im Artikel schauen wir uns an, was es mit React Hooks auf sich hat und was das mit Funktionskomponenten zu tun hat. Wir betrachten einige der Standard-Hooks und sehen, wie eigene Hooks gebaut werden können. Zum Schluss werfen wir einen Blick auf Third-Party-Bibliotheken aus der React-Community und darauf, wie sie das neue Hooks API adaptieren.

React erlaubt im Prinzip zwei Arten von Komponenten: Klassenkomponenten und Funktionskomponenten. Die Namen beziehen sich darauf, wie die Komponenten gebaut werden: Mit Klassen oder mit Funktionen. Für Klassenkomponenten kann bei einem reinem JavaScript-Set-up die mit ECMAScript 2015 eingeführte class-Syntax oder können bei TypeScript dessen Klassendefinitionen genutzt werden. Eine Klassenkomponente erweitert React.Component und muss in jedem Fall eine render-Methode besitzen. Diese render-Methode gibt eine DOM-Repräsentation zurück, die beschreibt, wie die Komponente gerade aussehen soll. Sie wird von React bei Bedarf aufgerufen und dazu genutzt, den tatsächlichen DOM-Baum im Browser entsprechend zu updaten.

Die einfachere, bisher aber auch weniger mächtige Variante sind Funktionskomponenten. Sie bestehen nur aus einer Funktion, die – vergleichbar mit der render-Methode bei Klassenkomponenten – eine DOM-Repräsentation zurückliefern. Um Komponenten zu parametrisieren, werden sogenannte Props benutzt. Bei der Benutzung einer Komponente werden diese Props als Attribute, vergleichbar mit XML-Attributen, am Tag der Komponente gesetzt. Bei Funktionskomponenten werden die Props in Form eines Objekts als Parameter an die Funktion übergeben. Bei Klassenkomponenten kann auf Props über die lokale Variable this.props zugegriffen werden. Von außen betrachtet sind beide Komponentenarten äquivalent. Man sieht einer Komponente also bei der Benutzung nicht unmittelbar an, ob sie als Funktion oder Klasse implementiert wurde. In Listing 1 sind drei Komponenten zu sehen, die in ihrer Darstellung äquivalent sind und die verschiedenen Arten zeigen. Auch die beiden Varianten der Funktionskomponenten sind im Prinzip gleich – einmal kommt die klassische function-Syntax und einmal die modernere Arrow-Function-Schreibweise zum Einsatz.

Listing 1

// Klassenkomponente class HelloClass extends React.Component { render() { return ( <div> <p>Hello {this.props.name}</p> </div> ) } } // Funktionskomponente function HelloFunction(props) { return ( <div> <p>Hello {props.name}</p> </div> ) } // Funktionskomponente als Arrow-Function const HelloArrowFunction = props => ( <div> <p>Hello {props.name}</p> </div> ) // Benutzung <HelloClass name="Luise" /> <HelloFunction name="Gustaf" /> <HelloArrowFunction name="Marie" />

Lokaler Zustand

Bis hierhin gibt es keine wesentlichen Unterschiede zwischen Funktionskomponenten und Klassenkomponenten, abgesehen vom persönlichen Geschmack. Die Mächtigkeit von Funktionskomponenten ist damit für bisherige Versionen von React auch abschließend beschrieben: Sie nehmen Daten entgegen und zeigen sie auf die gewünschte Art und Weise an. Klassenkomponenten können dagegen noch einiges mehr: Komponenten (das gilt für beide Varianten) dürfen ihre Props nicht verändern. Um veränderlichen Zustand umzusetzen, besitzen Klassenkomponenten einen sogenannten State. Auf ihn kann mittels this.state lesend zugegriffen werden. Um Änderungen am lokalen Zustand durchzuführen, muss die Methode this.setState aufgerufen werden. Nach einem Aufruf wird React ein Neu-Rendering der Komponente veranlassen. Dazu wird es, wie beschrieben, die render-Methode aufrufen, in der nun auf den neuen Zustand zugegriffen werden kann, um das Rendering-Ergebnis entsprechend zu beeinflussen. Die render-Methode bildet also im Prinzip die Props und den State auf eine UI-Beschreibung ab. In Listing 2 ist ein Beispiel zu sehen.

Listing 2

class Counter extends React.Component { constructor() { super() this.state = { counter: 0, } } increateCounter() { this.setState(prevState => { return { counter: prevState.counter + 1 } }) } render() { return ( <div> <p>Counter: {this.state.counter}</p> <button onClick={() => this.increateCounter()}>Increase</button> </div> ) } }

Lifecycle-Methoden

Funktionskomponenten steht dieser State-Mechanismus nicht zur Verfügung. Ein weiteres Feature, das Klassenkomponenten vorbehalten ist, sind Lifecycle-Methoden. Damit ist es möglich, auf bestimmte Ereignisse im Lebenszyklus einer Komponente zu reagieren und Code auszuführen. Ein Beispiel sind componentDidMount und componentWillUnmount. Sie werden aufgerufen, sobald die Komponente in den DOM-Baum eingehängt bzw. wieder entfernt wird. Ein häufiger Anwendungsfall ist das Laden von Daten, sobald die Komponente eingehängt wurde. Dazu löst man eine Ladeaktion in der componentDidMount-Methode aus und aktualisiert den lokalen State, sobald das Ergebnis da ist. In der Zwischenzeit zeichnet man einen Ladeindikator. Die componentWillUnmount-Methode ist notwendig, um eventuell noch laufende Aktionen sauber zu beenden. Sollte die Komponente beispielsweise bereits entfernt worden sein, während der Request noch läuft, kommt es zu einem Memory Leak, sobald der Request beendet wird, da im Callback eine dann nicht mehr existente Komponente upgedatet werden soll. Aus diesem Grund müssen wir sämtliche noch laufende Requests, Timer oder Subscriptions in der componentWillUnmount-Methode aufräumen. Listing 3 zeigt ein Beispiel mit dem fetch-API. Da in JavaScript laufende Promises nicht einfach beendet werden können, benutzen wir einen AbortController, der ebenfalls Teil der DOM-Spezifikation ist, um den fetch-Aufruf zu beenden.

Listing 3

class ShowRemoteData extends React.Component { abortController = new AbortController() constructor() { super() this.state = { ready: false, data: undefined, } } componentDidMount() { fetch("/some-data.json", { signal: this.abortController.signal }) .then(result => result.json()) .then(data => { this.setState({ ready: true, data: data.value, }) }) } componentWillUnmount() { this.abortController.abort() } render() { if (this.state.ready) { return ( <div> <p>Data: {this.state.data}</p> </div> ) } else { return <p>Loading</p> } } }

Wie beim lokalen Zustand haben Funktionskomponenten auch keinen Zugriff auf diese Lifecycle-Methoden. Es war mit Funktionskomponenten bisher also beispielsweise nicht möglich, Daten dynamisch nachzuladen. Diese Einschränkungen für Funktionskomponenten sind zwar nicht wirklich kritisch, weil ja bei Bedarf einfach auf Klassenkomponenten zurückgegriffen werden kann. Auch ein Refactoring von Funktionskomponente zu Klassenkomponente ist vergleichsweise einfach, da die Funktion ja im Wesentlichen der render-Methode der Klasse entspricht. Eine gewisse Unzufriedenheit blieb aber doch bestehen: Auf der einen Seite mögen viele React-Entwickler*innen die Einfachheit von Funktionskomponenten, auch weil sie den funktionalen Charakter von React so gut transportieren. Auf der anderen Seite kommt man um Klassenkomponenten doch nicht herum. Wann sollte man nun also Funktionskomponenten, wann Klassenkomponenten verwenden? In der React-Community haben sich aus dieser Situation heraus gewisse Patterns etabliert. Man versucht, so viel Code wie möglich in Funktionskomponenten zu packen. Diese übernehmen möglichst sämtliche visuellen Aspekte wie Layout, Styling und die Darstellung der Daten. Zustandsmanagement, Seiteneffekte und Ähnliches werden dagegen in Klassenkomponenten isoliert. Trot...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang