Ein Jahr React Hooks-API – eine Bilanz
Seite 3: Die Hooks-API im Überblick
Die Hooks-API sollte diese und andere Probleme vermeiden. Der Name stammt von der Tatsache, dass Entwickler sich mit der API in den React-Lebenszyklus "einhaken" (to hook into) und damit eine Komponentenfunktion um zusätzliche Features erweitern können. Hooks sind dabei technisch betrachtet normale JavaScript-Funktionen, deren Verwendung allerdings einigen Einschränkungen unterliegt. Eine solche ist, dass Entwickler Hooks nur innerhalb von Funktionskomponenten (und anderen Hooks) verwenden können. Diese können Anwender allerdings weiterhin in Klassenkomponenten einbinden und andersherum diese in Funktionskomponenten nutzen. Für den Verwender einer Komponente ist es nach wie vor transparent, ob die Komponente als Klasse oder Funktion und mit Hooks oder ohne implementiert ist.
Einer der wichtigsten Hooks ist useState
, mit dem eine Funktionskomponente Zustand verwalten kann. Die Greeting-Komponente könnte mit der Hooks-API wie folgt aussehen:
import React from "react";
function Greeting({onGreet}) {
const [greeting,setGreeting] = React.useState("");
function handleButtonClick() {
onGreet(greeting);
}
return <>
<input value={greeting}
onChange={e => setGreeting(e.target.value)}
/>
<button onClick={handleButtonClick}>
Greet
</button>
</>
}
Die useState
-Funktion erzeugt einen Zustand für die Komponente. Im Gegensatz zum State in einer Klassenkomponente, kann eine Komponente mit useState
mehrere Einzelzustände erhalten, in dem die Komponente den Hook mehrfach aufruft. Es ist für Entwickler nicht notwendig, dem useState
-Hook ein Objekt zu übergeben.
Offensichtlich an dem Beispiel ist, dass man kein this
mehr verwenden muss, da die Komponente als Funktion und nicht als Klasse implementiert ist. Außerdem liefert der useState
-Hook den aktuellen Wert (greeting) sowie eine Funktion zum Ändern des Zustands (setGreeting
) in einem Array zurück. Üblicherweise greift man auf die beiden Werte wie oben mit dem ES6-Destrukturierungsoperator zu, der sogar bei erfahreneren JavaScript-Entwicklern nicht allzu bekannt sein dürfte. Die React-Dokumentation widmet dem Thema einen eigenen Eintrag.
Nicht offensichtlich ist, wie der Hook überhaupt funktioniert. Beim ersten Rendern der Komponente liefert der Hook als aktuellen Zustand den übergebenen Wert zurück (im Beispiel ein Leerstring). Die zurückgegebene setter
-Funktion (setGreeting
) kann den Wert verändern.
Genau wie bei der Verwendung von setState
an der Klassen-API führt auch der Aufruf von setGreeting
nicht nur zur Aktualisierung des Zustands, sondern zum kompletten Neurendern der Komponente. Technisch bedeutet das, dass React die Funktion Greeting
ausführt. Bei der erneuten Ausführung der Funktion liefert useState
nun nicht mehr den Initialwert (Leerstring), sondern den zuletzt gesetzten Wert zurück. Die Komponente kann also, genau wie in der Klassen-API, abhängig vom aktualisierten Zustand jetzt die komplette, aktualisierte UI rendern. Dieses Verhalten, insbesondere das Ignorieren des Parameters von useState
in allen Folgeaufrufen, ist der useState
-API nicht anzusehen. Entwickler müssen es kennen, um die Funktionsweise zu verstehen. Auch deutet nichts darauf hin, wie die Hooks-API unter der Haube implementiert sein könnte und warum es für React möglich ist, den Zustand beim erneuten Rendern der Komponente in der Funktion wieder korrekt zur Verfügung zu stellen, insbesondere da es erlaubt ist, beliebig viele useState
-Aufrufe in einer Komponente zu verwenden:
import React from "react";
function Greeting({onGreet}) {
const [greeting,setGreeting] = React.useState("");
const [name,setName] = React.useState("");
...
}
Das funktioniert, da sich React die Aufrufe der einzelnen Hook-Funktionen intern in einer Art Tabelle zu jeder Komponente merkt und beim erneuten Ausführen der Komponente die Werte daraus liest. Obwohl das ein Implementierungsdetail ist, hat es Konsequenzen für Entwickler von Komponenten, sodass das Verwenden von Hook-Funktionen gewissen Regularien unterliegt. Daher müssen Entwickler Hook-Funktionen immer in derselben Reihenfolge innerhalb einer Komponente ausführen. Es ist nicht erlaubt, Hooks in einer Schleife oder einer Funktion innerhalb einer Komponente zu verwenden. Auch die bedingte Ausführung von Hooks ist nicht gestattet.
Das folgende Beispiel verwendet den useContext
-Hook, mit dem Anwender einen React Context abfragen können. Das ist das Pendant zum contextType
-Property an der Klassen-API beziehungsweise dem Render-Property der Context-Provider-Komponente. Im Beispiel ist im fiktiven LoginContext
hinterlegt, ob der aktuelle Benutzer sich angemeldet hat. Wenn der Benutzer nicht angemeldet ist, soll ein Redirect erfolgen, sonst erscheint ein Eingabefeld. Der Code ist allerdings in der folgenden Form nicht erlaubt, da der zweite Hook (useState
) nicht in allen Fällen aufgerufen wird:
function SettingsForm(props) {
const login = React.useContext(LoginContext);
if (!login.loggedIn) {
return <Redirect to="/login" />
}
const [ favColor, setFavColor ] = React.useState("blue");
return <input value={favColor} onChange={...} />
}
Korrekt wäre es, den useState
-Aufruf vor die if
-Abfrage zu verlagern. Das ist in der Praxis kein großes Problem, zeigt aber, dass die Hooks-API Entwicklern einen gewissen Programmierstil aufzwingt.
Um sicherzustellen, dass Entwickler die Regeln zum Verwenden der Hooks-API korrekt einhalten, hat das React-Team sogar ein ESLint-Plug-in veröffentlicht, das die wichtigsten Regeln überprüft und bei deren Verletzung eine Fehlermeldung ausgibt.
Seiteneffekte in Funktionskomponenten
Ein weiterer wichtiger Hook ist useEffect
, der sogenannte Seiteneffekte in Funktionskomponenten ermöglicht. Dazu gehören das Laden von Daten, das Starten beziehungsweise Stoppen von Timern oder das Arbeiten mit dem nativen DOM. Der Hook ist damit gewissermaßen das Pendant zu Lifecycle-Methoden wie componentDidMount
oder componentDidUpdate
der Klassen-API.
Das folgende Listing zeigt zunächst eine vereinfachte Klassenkomponente, die Daten für einen Blogbeitrag lädt:
class BlogPost extends React.Component {
state = {}
componentDidMount() {
this.loadPost(this.props.postId);
}
componentDidUpdate(prevProps) {
if (!this.props.postId !== propProps.prevId) {
this.loadPost(this.props.postId);
}
loadPost(postId) {
fetch(`/api/blog/${postId}`).
then(response => response.json())
.then(json => setState({blogPost: json});
}
render() {
if (!this.state.blogPost) {
return <h1>Bitte Geduld, Blog-Post wird geladen...</h1>
}
return ...;// Geladenen Blog-Post rendern
}
}
Die Komponente erwartet die ID eines Blog-Posts, lädt ihn und zeigt ihn an, sobald die Daten auf dem Client angekommen sind. Bemerkenswert ist, dass Entwickler mindestens zwei Lifecycle-Methoden implementieren müssen, damit die Komponente korrekt funktioniert: componentDidMount
für das initiale Rendern und componentDidUpdate
, um zu prüfen, ob eine neue beziehungsweise veränderte ID vorliegt, sodass die Komponente einen neuen Beitrag laden muss.
In anderen Fällen müssen Entwickler sogar noch eine dritte Lifecycle-Methode, componentDidUnmount
implementieren, um beispielsweise Ressourcen wieder freizugegeben. Das Feature "Laden eines Blogbeitrags" ist folglich auf mehrere Lifecycle-Methoden aufgeteilt. React-Entwickler müssen wissen, welche der Lifecycle-Methoden je nach Anwendungsfall zu implementieren sind und wie diese zusammen spielen.
useEffect
kann den kompletten Code an einer Stelle zusammenfassen. Die Logik, hier das Laden der Daten, erhält der Hook in einer Callback-Funktion als ersten Parameter. Als zweiten bekommt er in einem Array Werte übergeben, die bestimmen, wann es zum Ausführen der übergebenen Callback-Funktion kommen soll. Der Hook wird erstmalig nach dem initialen Rendern der Komponente ausgeführt. Wenn sich nach einem erneuten Rendern mindestens einer der Werte in dem Array ändert, kommt es erneut zur Ausführung des Hooks. Das obige Beispiel kann mit dem useEffect
wie folgt aussehen. Es lädt ebenfalls einen neuen Blogbeitrag, sobald sich die als Property übergebene postId
verändert:
function BlogPost({postId}) {
const [blogPost, setBlogPost] = React.useState(null);
React.useEffect( () => {
fetch((`/api/blog/${postId}`)
.then(response => response.json())
.then(json => setBlogPost (json));
}, [postId]);
if (!blogPost) {
return <h1>Bitte Geduld, Blog-Post wird geladen...</h1>
}
return ...;// Geladenen Blog Post rendern
Im useEffect
-Hook sind nun alle Lifecycle-Phasen direkt um das implementierte Feature zusammengefasst und nicht mehr auf unterschiedliche Callbacks verteilt. Damit sollen Entwickler leichter verstehen, wie ein bestimmtes Feature funktioniert. Durch das Dependency-Array entfällt außerdem die tendenziell fehleranfällige Prüfung, ob sich ein Property verändert hat.
Allerdings ist das Dependency-Array zunächst gewöhnungsbedürftig und Entwickler müssen genau darauf achten, welche Parameter sie dort angegeben müssen. Das ist nicht immer ganz offensichtlich.
Auch kann es passieren, dass die Applikation auf Grund der Eigenschaften von Closures auf einen veralteten ("stale") Wert aus der Komponente zugreift, beispielsweise den State. Allerdings hilft das ESLint-Plug-in und weist auf mögliche fehlende Parameter in dem Array hin.