25 Custom Hooks - Die Kunst der wiederverwendbaren State-Logik

Nachdem wir uns mit den grundlegenden React Hooks wie useState, useEffect und useContext vertraut gemacht haben, ist es an der Zeit, eine der elegantesten Möglichkeiten zur Code-Wiederverwendung in React kennenzulernen: Custom Hooks. Diese selbst erstellten Hooks ermöglichen es uns, komplexe State-Logik zu extrahieren und zwischen verschiedenen Komponenten zu teilen, ohne dabei auf umständliche Patterns zurückgreifen zu müssen.

25.1 Die Motivation hinter Custom Hooks

In der Entwicklung von React-Anwendungen stoßen wir häufig auf Situationen, in denen mehrere Komponenten ähnliche State-Logik benötigen. Vielleicht müssen verschiedene Komponenten Daten von einer API laden, mit dem localStorage interagieren oder auf Window-Events reagieren. Vor der Einführung von Hooks war das Teilen solcher Logik zwischen Komponenten eine Herausforderung, die oft zu komplexen Lösungen wie Higher-Order Components oder Render Props führte.

Custom Hooks lösen dieses Problem auf elegante Weise. Sie sind im Grunde normale JavaScript-Funktionen, die andere Hooks verwenden können. Der entscheidende Unterschied zu gewöhnlichen Funktionen liegt darin, dass Custom Hooks die Regeln der React Hooks befolgen und dadurch in das React-Ökosystem integriert sind. Durch die Konvention, dass ihr Name mit “use” beginnt, erkennt React diese Funktionen als Hooks und kann sicherstellen, dass sie korrekt verwendet werden.

25.2 Die Anatomie eines Custom Hooks

Ein Custom Hook folgt einem einfachen Muster: Er ist eine Funktion, die mit “use” beginnt und andere Hooks verwendet. Diese Funktion kann Parameter entgegennehmen und Werte zurückgeben - genau wie jede andere JavaScript-Funktion auch. Der Unterschied liegt in der Möglichkeit, React-spezifische Funktionalität wie State, Effects oder Context zu nutzen.

Betrachten wir ein einfaches Beispiel: Ein Hook, der den aktuellen Wert eines Eingabefeldes verwaltet. Ohne Custom Hook würden wir in jeder Komponente, die ein Eingabefeld benötigt, denselben State- und Handler-Code schreiben. Mit einem Custom Hook können wir diese Logik extrahieren und wiederverwenden.

Die Struktur eines solchen Hooks folgt einem klaren Muster. Zuerst definieren wir die Funktion mit einem Namen, der mit “use” beginnt. Innerhalb der Funktion verwenden wir die benötigten React Hooks wie useState oder useEffect. Schließlich geben wir die Werte zurück, die die aufrufende Komponente benötigt - dies kann ein einzelner Wert, ein Array oder ein Objekt sein.

25.3 Die Regeln der Custom Hooks

React enforct zwei fundamentale Regeln für Custom Hooks, die für ihr korrektes Funktionieren essentiell sind. Die erste Regel besagt, dass der Name eines Custom Hooks immer mit “use” beginnen muss. Dies ist mehr als nur eine Namenskonvention - es ist ein Signal an React und die Entwicklungstools, dass diese Funktion den Hook-Regeln folgt. Linting-Tools wie ESLint mit dem React Hooks Plugin nutzen diese Konvention, um sicherzustellen, dass Hooks korrekt verwendet werden.

Die zweite Regel ist, dass Custom Hooks nur in React-Funktionskomponenten oder in anderen Custom Hooks aufgerufen werden dürfen. Sie dürfen nicht in normalen JavaScript-Funktionen, Schleifen, Bedingungen oder verschachtelten Funktionen verwendet werden. Diese Einschränkung stellt sicher, dass React die Reihenfolge der Hook-Aufrufe zwischen Renders konsistent halten kann.

Ein häufiges Missverständnis ist, dass Custom Hooks State zwischen Komponenten teilen. Das ist nicht der Fall. Jede Komponente, die einen Custom Hook verwendet, erhält ihre eigene isolierte Instanz des States. Wenn zwei Komponenten denselben Custom Hook verwenden, teilen sie sich nicht den State - sie haben jeweils ihre eigene Kopie. Dies ist ein wichtiger Unterschied zu globalen State-Management-Lösungen.

25.4 Praktische Anwendungsfälle

Custom Hooks glänzen besonders in Situationen, in denen wir wiederkehrende Patterns in unserer Anwendung identifizieren. Ein klassisches Beispiel ist die Interaktion mit dem Browser localStorage. Viele Komponenten müssen möglicherweise Werte im localStorage speichern und lesen. Anstatt diese Logik in jeder Komponente zu duplizieren, können wir einen useLocalStorage Hook erstellen, der diese Funktionalität kapselt.

Ein weiterer häufiger Anwendungsfall ist das Laden von Daten von einer API. Fast jede moderne React-Anwendung muss Daten von einem Server abrufen. Ein useFetch oder useAPI Hook kann die gesamte Logik für das Laden, die Fehlerbehandlung und das Loading-State-Management kapseln. Komponenten können dann einfach diesen Hook verwenden und erhalten strukturierte Daten, Loading-States und Fehlerinformationen.

Window-Events sind ein weiteres Gebiet, wo Custom Hooks ihre Stärke zeigen. Ob es um die Fenster-Größe, die Scroll-Position oder die Netzwerk-Verbindung geht - all diese Informationen können in wiederverwendbaren Hooks gekapselt werden. Ein useWindowSize Hook kann beispielsweise die aktuelle Fenstergröße tracken und automatisch aktualisieren, wenn das Fenster verändert wird.

Form-Handling ist ebenfalls ein Bereich, der von Custom Hooks profitiert. Ein useForm Hook kann die gesamte Logik für Formular-State, Validierung und Submission handling kapseln. Dies reduziert nicht nur Code-Duplikation, sondern macht auch das Testing einfacher, da die Form-Logik isoliert getestet werden kann.

25.5 Die Kunst der Hook-Komposition

Eine der mächtigsten Eigenschaften von Custom Hooks ist ihre Fähigkeit zur Komposition. Custom Hooks können andere Custom Hooks verwenden, was es ermöglicht, komplexe Funktionalität aus einfacheren Bausteinen zusammenzusetzen. Diese Komposition folgt dem gleichen Prinzip wie die Komposition von React-Komponenten.

Stellen Sie sich vor, Sie haben einen useAuth Hook, der die Authentifizierung handhabt, und einen useFetch Hook für API-Calls. Sie können einen useAuthenticatedFetch Hook erstellen, der beide kombiniert und automatisch Authentifizierungs-Token zu API-Requests hinzufügt. Diese Art der Komposition macht den Code modular und testbar.

Die Komposition von Hooks ermöglicht es auch, schrittweise Komplexität aufzubauen. Sie können mit einfachen, fokussierten Hooks beginnen und diese dann zu komplexeren Hooks kombinieren, die spezifische Business-Logik implementieren. Jeder Hook bleibt dabei für sich testbar und verständlich.

25.6 Performance-Überlegungen

Bei der Erstellung von Custom Hooks ist es wichtig, Performance-Aspekte zu berücksichtigen. Custom Hooks sollten die gleichen Performance-Optimierungen verwenden wie reguläre Komponenten. Dies bedeutet den gezielten Einsatz von useMemo und useCallback, um unnötige Neuberechnungen und Re-Renders zu vermeiden.

Ein häufiger Fehler ist es, bei jedem Render neue Objekte oder Funktionen aus einem Custom Hook zurückzugeben. Dies kann zu unnötigen Re-Renders in den konsumierenden Komponenten führen. Wenn ein Hook ein Objekt zurückgibt, sollten die enthaltenen Funktionen mit useCallback gememoized werden, um ihre Referenz-Stabilität zu gewährleisten.

Auch die Abhängigkeiten von useEffect innerhalb von Custom Hooks verdienen besondere Aufmerksamkeit. Fehlende oder überflüssige Abhängigkeiten können zu Bugs oder Performance-Problemen führen. Die ESLint-Regel “exhaustive-deps” ist hier ein wertvolles Werkzeug, um sicherzustellen, dass alle Abhängigkeiten korrekt deklariert sind.

25.7 TypeScript und Custom Hooks

Die Verwendung von TypeScript mit Custom Hooks erhöht die Typsicherheit und Entwicklererfahrung erheblich. Custom Hooks sollten vollständig typisiert sein, sowohl in ihren Parametern als auch in ihren Rückgabewerten. Dies macht die API des Hooks klar und selbstdokumentierend.

Bei der Typisierung von Custom Hooks gibt es einige Dinge zu beachten. Generische Typen ermöglichen es, flexible Hooks zu erstellen, die mit verschiedenen Datentypen arbeiten können. Die Verwendung von “as const” Assertions kann helfen, präzisere Typen für Array-Rückgabewerte zu erhalten. Discriminated Unions sind nützlich für Hooks, die verschiedene Zustände repräsentieren können, wie Loading, Success und Error States.

25.8 Testing von Custom Hooks

Ein großer Vorteil von Custom Hooks ist ihre Testbarkeit. Da sie reine JavaScript-Funktionen sind (die allerdings Hook-Regeln folgen), können sie isoliert von Komponenten getestet werden. Die React Testing Library bietet mit @testing-library/react-hooks spezielle Utilities für das Testen von Custom Hooks.

Beim Testen von Custom Hooks ist es wichtig, verschiedene Szenarien abzudecken: initiale Werte, State-Updates, Effect-Cleanup und Edge Cases. Asynchrone Hooks erfordern besondere Aufmerksamkeit, um Race Conditions und Memory Leaks in Tests zu vermeiden.

25.9 Troubleshooting

Bei der Arbeit mit Custom Hooks gibt es einige häufige Fehler, die vermieden werden sollten. Ein klassischer Fehler ist es, Hooks bedingt aufzurufen. Auch innerhalb von Custom Hooks müssen alle Hook-Aufrufe immer in der gleichen Reihenfolge erfolgen. Bedingte Logik sollte innerhalb der Hooks stattfinden, nicht um sie herum.

Ein weiterer häufiger Fehler ist das Vergessen von Cleanup-Funktionen in useEffect. Wenn ein Custom Hook Event-Listener registriert oder Subscriptions erstellt, muss er auch für deren Cleanup sorgen. Andernfalls können Memory Leaks entstehen, besonders wenn Komponenten häufig gemountet und unmountet werden.

Die übermässige Nutzung von Custom Hooks ist ebenfalls ein Antipattern. Nicht jede Logik muss in einen Custom Hook extrahiert werden. Wenn die Logik nur in einer einzigen Komponente verwendet wird und nicht besonders komplex ist, ist es oft besser, sie direkt in der Komponente zu belassen. Custom Hooks sollten erstellt werden, wenn tatsächlicher Bedarf für Wiederverwendung besteht oder wenn sie die Lesbarkeit signifikant verbessern.