useEncapsulation: Warum jeder React Hook ein Zuhause verdient
Custom Hooks sind nicht nur praktisch. Sie sind das wichtigste Architektur-Werkzeug in modernem React. Dieser Artikel erklärt wann, warum und wie man Hooks kapselt. Mit Patterns, Anti-Patterns und Praxisbeispielen.
useEncapsulation: Warum jeder React Hook ein Zuhause verdient
Es gibt einen Moment im Leben jeder React-Komponente, in dem sie eine Grenze überschreitet. Es fängt harmlos an, ein einzelnes useState, vielleicht ein useEffect zum Daten laden. Dann fügt jemand einen Toggle hinzu. Dann ein Formularfeld. Dann ein Subscription. Ehe man sichs versieht, liest sich die Komponenten-Funktion wie ein Bewusstseinsstrom: State-Deklarationen, Event-Handler, Effects und Refs wild durcheinander, ohne erkennbare Struktur.
Der Code funktioniert noch. Aber ihn zu lesen erfordert, die gesamte Funktion im Kopf zu behalten und mental zu gruppieren: „diese drei Zeilen gehören zusammen" und „dieser Handler gehört zu diesem State." Die Komponente ist zu einer flachen Liste von Implementierungsdetails ohne Nähte geworden.
Custom Hooks lösen dieses Problem. Nicht durch Abstraktion um der Abstraktion willen, sondern indem sie zusammengehöriger Logik einen Namen und eine Grenze geben. Sie sind Reacts Antwort auf Kapselung, und das am meisten unterschätzte Architektur-Werkzeug im Ökosystem.
Das Problem: Verstreuter State
Betrachten wir eine Komponente, die ein Modal und eine Sucheingabe verwaltet:
Vier State-Variablen. Vier Handler. Ein Effect. Alles in denselben Funktionskörper geworfen. Der Modal-State (isModalOpen, openModal, closeModal) hat nichts mit der Such-Logik (searchQuery, handleSearchChange, resetSearch) zu tun, aber sie sitzen nebeneinander, nur getrennt durch die Reihenfolge, in der man sie zufällig geschrieben hat.
Das ist kein Lesbarkeitsproblem. Es ist ein strukturelles Problem. Wenn die Komponente wächst, steigen die mentalen Kosten, um zu verstehen welche Teile zusammengehören, linear mit jedem neuen Hook-Aufruf.
Die Lösung: Zusammengehöriger Logik einen Namen geben
Jetzt liest sich die Komponente wie ein Inhaltsverzeichnis:
tsx
functionUserDirectory(){
const modal =useModal();
const search =useSearch(fetchUsers);
return(
// ... JSX mit modal.isOpen, search.results, etc.
);
}
Zwei Zeilen. Zwei benannte Konzepte. Die Implementierungsdetails sind nicht verschwunden, sie wurden an einen Ort verlagert, wo man sie isoliert verstehen kann.
Beachte, dass der extrahierte useSearch-Hook jetzt ein Cleanup-Flag (cancelled) enthält, das in der ursprünglichen Inline-Version fehlte. Das ist kein Zufall, die Isolierung der Fetch-Logik in einer eigenen Funktion machte das fehlende Cleanup offensichtlich. Wenn ein Effect zwischen zwanzig anderen Zeilen in einer Komponente lebt, übersieht man leicht ein fehlendes Return. In einem fokussierten Hook springt die Lücke einem ins Auge.
Das bringt mehr als eine kürzere Komponente:
Abhängigkeiten werden explizit. Die Funktionssignatur eines Custom Hooks ist seine Abhängigkeitsliste. useSearch(fetcher) verrät auf einen Blick: Dieser Hook hängt von einer Fetcher-Funktion ab. Sonst nichts von der Außenwelt.
Interna können sich frei weiterentwickeln. Der useModal-Hook verwendet heute useState. Morgen könntest du ihn auf useReducer umbauen für Exit-Animationen. Die konsumierende Komponente ändert sich nicht, sie ruft weiterhin modal.open() auf und liest modal.isOpen. Der Vertrag ist stabil, auch wenn sich die Implementierung weiterentwickelt.
Testbarkeit verbessert sich dramatisch. Eine 200-Zeilen-Komponente zu testen, die UI mit Datenabruf und Event-Handling vermischt, ist mühsam. useSearch isoliert mit einem gemockten Fetcher zu testen ist unkompliziert. Man testet die Logik, nicht das DOM.
Wiederverwendung passiert natürlich. Du hast useModal nicht für Wiederverwendung geschrieben. Du hast ihn für Kapselung geschrieben. Aber jetzt kann jedes Modal in deiner App ihn verwenden. Wiederverwendung ist ein Nebeneffekt guter Struktur, nicht das Ziel.
Die konsumierende Komponente ruft weiterhin modal.open() auf. Sie hat jetzt nur zusätzlich Zugriff auf modal.isAnimating, wenn sie es braucht.
Benennung: Der schwierigste Teil
Der Name eines Custom Hooks ist seine Dokumentation. Wenn er falsch gewählt ist, wird die Abstraktion zu einer Black Box, der niemand vertraut.
Das use-Präfix ist nicht verhandelbar
Reacts Linter erzwingt das use-Präfix. Ohne es kann React nicht überprüfen, ob deine Funktion die Regeln der Hooks befolgt (keine bedingten Aufrufe, keine Aufrufe in Schleifen). Das ist keine Konvention: es ist eine technische Anforderung.
Benennungsmuster
Muster
Wann verwenden
Beispiele
use + Substantiv
Verwaltet ein bestimmtes State-Stück
useModal, useAuth, useCart
use + Verb
Führt eine Aktion oder Seiteneffekt aus
useFetch, useDebounce, useIntersect
use + Substantiv + State
Betont State-Management
useFormState, useSelectionState
use + On/Handle + Event
Kapselt Event-Handler-Logik
useOnClickOutside, useOnKeyPress
Benenne das Verhalten, nicht die Implementierung
tsx
// Schlecht - der Name beschreibt die Implementierung
functionuseStateWithCallback(){...}
// Gut - der Name beschreibt das Verhalten
functionuseNotification(){...}
Ein Entwickler, der useNotification() liest, weiß was es tut. Ein Entwickler, der useStateWithCallback() liest, weiß wie es intern funktioniert: genau das Detail, das der Hook verbergen sollte.
Wann eine Funktion KEIN Hook sein sollte
Wenn deine Funktion intern keine React Hooks aufruft, gib ihr nicht das use-Präfix. Es ist eine normale Hilfsfunktion:
Das use-Präfix ist ein Versprechen: „Ich enthalte React-State oder -Effects." Dieses Versprechen zu brechen verwirrt sowohl den Linter als auch deine Kollegen.
Der Lackmustest
Wenn du die Funktion außerhalb einer React-Komponente aufrufen könntest und
sie würde trotzdem funktionieren, ist sie kein Hook. Verwende nicht das
use-Präfix.
Patterns
State + Handler
Die häufigste Custom-Hook-Form: Ein State mit seinen zugehörigen Handlern gruppieren.
tsx
functionuseCounter(initial =0){
const[count, setCount]=useState(initial);
const increment =useCallback(()=>setCount((c)=> c +1),[]);
const decrement =useCallback(()=>setCount((c)=> c -1),[]);
Kein useEffect. Kein extra State. Nur abgeleitete Werte. Das ist eines der mächtigsten und am wenigsten genutzten Patterns, viele Entwickler greifen zu useEffect + useState, wenn useMemo allein genügen würde.
Die useEffect-Falle vermeiden
Wenn du useEffect verwendest, um State basierend auf anderem State oder
Props zu aktualisieren, willst du fast sicher useMemo oder eine direkte
Berechnung während des Renderns. useEffect ist für Seiteneffekte. Dinge, die
außerhalb von Reacts Rendering passieren, wie API-Aufrufe, Subscriptions
oder DOM-Messungen.
Komposition
Hooks können andere Hooks aufrufen. So baut man komplexes Verhalten aus einfachen Bausteinen:
useDebouncedSearch komponiert useDebounce, ohne dessen Interna zu kennen. Jeder Hook löst ein Problem. Zusammen lösen sie ein komplexes.
Return-Form: Objekt vs. Tuple
Verwende Tuples, wenn der Hook zwei oder drei verwandte Werte zurückgibt (wie useState selbst). Verwende Objekte, wenn der Hook mehr als drei Werte zurückgibt oder die Werte keine natürliche Reihenfolge haben. Objekte erlauben Destructuring nach Name, was selbstdokumentierend ist:
tsx
// Tuple - spiegelt useState, Position traegt Bedeutung
Wenn dein Hook unzusammenhängende Belange verwaltet, kapselt er nicht, er verlagert nur das Chaos. Jeder Belang sollte ein eigener Hook sein: useAuth, useTheme, useNotifications, useCart, useMenu.
2. Die voreilige Abstraktion
Nicht jeder useState-Aufruf braucht einen Custom Hook:
tsx
// Dieser Hook bringt keinen Mehrwert
functionuseIsOpen(){
const[isOpen, setIsOpen]=useState(false);
return{ isOpen, setIsOpen };
}
Das ist nur useState mit Extra-Schritten. Ein Custom Hook sollte seine Existenz verdienen, indem er mehrere zusammengehörige Logik-Stücke gruppiert. State, Effects, Handler, abgeleitete Werte, nicht indem er ein einzelnes Primitiv umwickelt.
3. useEffect als State-Synchronisierer
Das häufigste Anti-Pattern in React-Codebasen:
tsx
// Das nicht machen
functionuseFormattedPrice(cents:number){
const[formatted, setFormatted]=useState("");
useEffect(()=>{
setFormatted(`$${(cents /100).toFixed(2)}`);
},[cents]);
return formatted;
}
Das erzeugt einen unnötigen Render-Zyklus: Rendern mit veraltetem Wert, Effect feuert, State-Update, erneutes Rendern mit korrektem Wert. Die Lösung ist trivial:
Oder noch einfacher, das muss überhaupt kein Hook sein:
tsx
functionformatPrice(cents:number):string{
return`$${(cents /100).toFixed(2)}`;
}
useEffect ist für Seiteneffekte: Netzwerk-Requests, Subscriptions, DOM-Mutationen, Timer. Wenn du es verwendest, um Daten aus Props oder State in anderen State zu transformieren, kämpfst du gegen React, statt mit ihm zu arbeiten.
4. Fehlende Bereinigung
Jede Subscription, jeder Timer oder Listener, der in einem useEffect eingerichtet wird, muss bereinigt werden:
Wenn dein Effect etwas hinzufügt (Listener, Subscription, Timer), muss er es
auch entfernen. Keine Ausnahmen. Reacts Strict Mode in der Entwicklung wird
deine Komponente unmounten und wieder mounten, um dir diese Bugs zu zeigen,
wenn du doppelte Effects feuern siehst, ist das die Prüfung in Aktion.
5. Veraltete Closures
Closures erfassen Variablen zum Zeitpunkt ihrer Erstellung. Wenn ein Callback auf State referenziert, aber nicht neu erstellt wird wenn sich der State ändert, liest er veraltete Werte:
tsx
// Bug: Der Alert zeigt immer den initialen Count
functionuseCounter(){
const[count, setCount]=useState(0);
const showCount =useCallback(()=>{
setTimeout(()=>alert(count),1000);
},[]);// 'count' fehlt in den Dependencies
return{ count, setCount, showCount };
}
Die eslint-plugin-react-hooks exhaustive-deps-Regel fängt das ab. Wenn der Linter sagt, eine Abhängigkeit fehlt, hat er fast immer Recht.
6. JSX aus Hooks zurückgeben
Hooks geben Daten und Handler zurück. Sie geben keine UI zurück:
Die Komponente bestimmt die UI. Der Hook verwaltet den State. Jeder macht das, worin er gut ist.
Wann man nicht extrahieren sollte
Kapselung ist nicht die Antwort auf jedes Problem. Manchmal ist eine Komponente mit drei useState-Aufrufen und zwei Handlern so wie sie ist perfekt lesbar. Einen Hook zu extrahieren, der nur einmal verwendet wird und nur eine State-Variable enthält, fügt eine Indirektionsebene hinzu, ohne Klarheit zu schaffen.
Eine gute Faustregel: Extrahiere, wenn die Logik ein Konzept mit einem Namen bildet. Wenn du den Hook nicht benennen kannst, ohne seine Implementierung zu beschreiben („useStateAndEffectFürDasDing"), gehört die Logik wahrscheinlich noch nicht in einen Hook. Warte, bis sich das Konzept herauskristallisiert: sei es durch Wiederverwendung, Komplexität, oder das einfache Bedürfnis, die Komponente zu lesen, ohne in Details zu ertrinken.
Das Ziel ist nicht null Hooks in Komponenten. Das Ziel ist, dass jeder Hook-Aufruf in einer Komponente sich wie ein Satz liest: Diese Komponente verwendet Authentifizierung, ein Modal und eine entprellte Suche. Wenn sich die Komponente wie ein Absatz voller Absichten liest statt wie eine Wand aus Mechanik, hast du die richtige Extraktionsebene gefunden.
Tooling: Maschinen die Disziplin durchsetzen lassen
Gute Gewohnheiten sind leichter aufrechtzuerhalten, wenn die Toolchain einem den Rücken stärkt. Man muss sich nicht allein auf Code Reviews verlassen, um verstreute Hooks, Gott-Hooks oder veraltete Closures zu finden. Mehrere ESLint-Regeln, einige React-spezifisch, einige allgemein, können die in diesem Artikel besprochenen Leitplanken automatisieren.
Die Grundlage. Jedes React-Projekt sollte dies aktiviert haben:
json
{
"rules":{
"react-hooks/rules-of-hooks":"error",
"react-hooks/exhaustive-deps":"warn"
}
}
rules-of-hooks stellt sicher, dass Hooks nie bedingt oder in Schleifen aufgerufen werden. exhaustive-deps fängt veraltete Closures ab, indem es warnt, wenn eine Abhängigkeit in useEffect, useMemo oder useCallback fehlt. Deaktiviere es nicht, wenn du dagegen kämpfst, muss wahrscheinlich der Code umstrukturiert werden, nicht die Regel unterdrückt.
Ab v6.1.1 kannst du auch additionalHooks verwenden, um die exhaustive-deps-Prüfung auf eigene Custom Hooks anzuwenden, die Dependency-Arrays akzeptieren:
Kyle Shevlins Plugin, das die Kernthese dieses Artikels durchsetzt: Verwende React Hooks nicht direkt in Komponenten. Seine einzige Regel, prefer-custom-hooks, warnt immer wenn useState, useEffect, useRef oder ein anderer React Hook direkt in einer Komponente statt in einem Custom Hook erscheint.
json
{
"plugins":["use-encapsulation"],
"rules":{
"use-encapsulation/prefer-custom-hooks":["warn"]
}
}
Du kannst bestimmte Hooks mit der allow-Option auf eine Whitelist setzen, falls strikte Durchsetzung für deine Codebasis zu aggressiv ist. Verwende das sparsam, ein gelegentliches eslint-disable ist besser als eine pauschale Ausnahme.
Schrittweise Einführung
Setze es anfangs auf "warn", nicht auf "error". Lass das Team die
Warnungen ein paar Sprints lang im Kontext sehen, bevor ihr entscheidet,
welche strikt durchgesetzt werden. Das vermeidet eine Wand aus Rot am ersten
Tag und gibt den Leuten Zeit, das Pattern zu verinnerlichen.
Gott-Hooks mit allgemeinen ESLint-Regeln erkennen
Es gibt kein dediziertes "Gott-Hook-Detektor"-Plugin. Aber allgemeine Komplexitätsregeln gelten für Hooks genauso wie für jede andere Funktion:
json
{
"rules":{
"max-lines-per-function":[
"warn",
{
"max":80,
"skipBlankLines":true,
"skipComments":true
}
],
"complexity":["warn",10],
"max-statements":["warn",15],
"max-depth":["warn",3]
}
}
max-lines-per-function erkennt Hooks, die zu gross geworden sind, um sie auf einen Blick zu verstehen. 80 Zeilen sind ein vernünftiger Startwert, genug für einen useReducer mit ein paar Handlern, eng genug um Hooks zu markieren, die fünf unzusammenhängende Belange verwalten.
complexity misst zyklomatische Komplexität, die Anzahl unabhängiger Pfade durch die Funktion. Ein Hook mit einer Komplexität von 15 hat zu viele Verzweigungen und sollte aufgeteilt werden.
max-statements begrenzt die Anzahl der Anweisungen. Wenn ein Hook 20 const-Deklarationen hat, macht er fast sicher zu viel.
max-depth erkennt tief verschachtelte Bedingungen und Schleifen in Hooks.
Keine davon ist React-bewusst, aber Hooks sind Funktionen, und diese Regeln funktionieren bei allen Funktionen.
Wenn du React 19+ mit dem React Compiler verwendest, enthält eslint-plugin-react-hooks jetzt zusätzliche Regeln wie react-hooks/purity und react-hooks/refs, die validieren, ob deine Komponenten und Hooks sicher für automatische Memoization sind. Diese erzwingen Kapselung nicht direkt, belohnen aber gut strukturierte Hooks - je einfacher und reiner dein Hook, desto mehr kann der Compiler optimieren.
Alles zusammen
Eine praktische ESLint-Konfiguration, die die meisten Patterns aus diesem Artikel durchsetzt:
Das wird nicht jedes Anti-Pattern finden. Kein Linter ersetzt Urteilsvermögen. Aber es verschiebt den Standard: Statt sich allein auf Disziplin zu verlassen, stupst die Toolchain einen in Richtung Kapselung, markiert Komplexität bevor sie zum Problem wird, und verhindert die häufigsten Korrektheitsfehler automatisch.
Praxisbeispiel: Ein vollständiger useForm-Hook
Um alles zusammenzuführen, hier ein produktionsreifer Form-Hook, der mehrere Patterns kombiniert. State-Management, abgeleiteter State, Validierung und sauberes API-Design:
Der Hook verwaltet Form-State, Validierung, Touched-Tracking und den Submission-Flow. Er rendert keine Inputs, entscheidet keine Fehler-Stile und diktiert kein Layout. Die Komponente besitzt die UI. Der Hook besitzt die Logik.