4. Februar 2026
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.
Sascha Becker
Author20 Min. Lesezeit
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:
tsxfunction UserDirectory() {const [isModalOpen, setIsModalOpen] = useState(false);const [searchQuery, setSearchQuery] = useState('');const [users, setUsers] = useState<User[]>([]);const [isLoading, setIsLoading] = useState(false);const openModal = () => setIsModalOpen(true);const closeModal = () => setIsModalOpen(false);const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {setSearchQuery(e.target.value);};const resetSearch = () => setSearchQuery('');useEffect(() => {setIsLoading(true);fetchUsers(searchQuery).then(setUsers).finally(() => setIsLoading(false));}, [searchQuery]);return (// ... JSX das alles oben Genannte verwendet);}
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
Die Modal-Logik in einen Custom Hook extrahieren:
tsxfunction useModal(initialState = false) {const [isOpen, setIsOpen] = useState(initialState);const open = () => setIsOpen(true);const close = () => setIsOpen(false);const toggle = () => setIsOpen((prev) => !prev);return { isOpen, open, close, toggle };}
Die Such-Logik in einen weiteren:
tsxfunction useSearch(fetcher: (query: string) => Promise<User[]>) {const [query, setQuery] = useState("");const [results, setResults] = useState<User[]>([]);const [isLoading, setIsLoading] = useState(false);const handleChange = (e: ChangeEvent<HTMLInputElement>) => {setQuery(e.target.value);};const reset = () => setQuery("");useEffect(() => {let cancelled = false;setIsLoading(true);fetcher(query).then((data) => {if (!cancelled) setResults(data);}).finally(() => {if (!cancelled) setIsLoading(false);});return () => {cancelled = true;};}, [query, fetcher]);return { query, results, isLoading, handleChange, reset };}
Jetzt liest sich die Komponente wie ein Inhaltsverzeichnis:
tsxfunction UserDirectory() {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 heuteuseState. Morgen könntest du ihn aufuseReducerumbauen für Exit-Animationen. Die konsumierende Komponente ändert sich nicht, sie ruft weiterhinmodal.open()auf und liestmodal.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.
useSearchisoliert mit einem gemockten Fetcher zu testen ist unkompliziert. Man testet die Logik, nicht das DOM. - Wiederverwendung passiert natürlich. Du hast
useModalnicht 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.
tsxtype ModalState = { isOpen: boolean; isAnimating: boolean };type ModalAction =| { type: "OPEN" }| { type: "CLOSE" }| { type: "ANIMATION_END" };function modalReducer(state: ModalState, action: ModalAction): ModalState {switch (action.type) {case "OPEN":return { isOpen: true, isAnimating: true };case "CLOSE":return { isOpen: false, isAnimating: true };case "ANIMATION_END":return { ...state, isAnimating: false };default:return state;}}function useModal(initialState = false) {const [state, dispatch] = useReducer(modalReducer, {isOpen: initialState,isAnimating: false,});const open = useCallback(() => dispatch({ type: "OPEN" }), []);const close = useCallback(() => dispatch({ type: "CLOSE" }), []);const toggle = useCallback(() => {dispatch(state.isOpen ? { type: "CLOSE" } : { type: "OPEN" });}, [state.isOpen]);return {isOpen: state.isOpen,isAnimating: state.isAnimating,open,close,toggle,};}
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 Implementierungfunction useStateWithCallback() { ... }// Gut - der Name beschreibt das Verhaltenfunction useNotification() { ... }
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:
tsx// Das ist KEIN Hook - es ruft keine Hooks auffunction sortUsers(users: User[], key: keyof User): User[] {return [...users].sort((a, b) => /* ... */);}// Das IST ein Hook - es verwendet useStatefunction useSortedUsers(users: User[], key: keyof User) {const [sortKey, setSortKey] = useState(key);const sorted = useMemo(() => sortUsers(users, sortKey), [users, sortKey]);return { sorted, sortKey, setSortKey };}
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.
tsxfunction useCounter(initial = 0) {const [count, setCount] = useState(initial);const increment = useCallback(() => setCount((c) => c + 1), []);const decrement = useCallback(() => setCount((c) => c - 1), []);const reset = useCallback(() => setCount(initial), [initial]);return { count, increment, decrement, reset };}
Einfach. Fokussiert. Jede Zeile dient einem einzigen Konzept.
Abgeleiteter State
Wenn du Werte aus bestehendem State berechnen musst, leite sie innerhalb des Hooks ab, statt sie separat zu speichern:
tsxfunction usePasswordStrength(password: string) {const strength = useMemo(() => {if (password.length === 0) return "none";if (password.length < 6) return "weak";if (password.length < 10) return "medium";const hasUpper = /[A-Z]/.test(password);const hasNumber = /\d/.test(password);const hasSpecial = /[^A-Za-z0-9]/.test(password);return hasUpper && hasNumber && hasSpecial ? "strong" : "medium";}, [password]);const colors = {none: "grey",weak: "red",medium: "orange",strong: "green",} as const;const color = colors[strength];return { strength, color };}
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:
tsxfunction useDebounce<T>(value: T, delay: number): T {const [debouncedValue, setDebouncedValue] = useState(value);useEffect(() => {const timer = setTimeout(() => setDebouncedValue(value), delay);return () => clearTimeout(timer);}, [value, delay]);return debouncedValue;}function useDebouncedSearch(fetcher: (q: string) => Promise<User[]>) {const [query, setQuery] = useState("");const debouncedQuery = useDebounce(query, 300);const [results, setResults] = useState<User[]>([]);const [isLoading, setIsLoading] = useState(false);useEffect(() => {if (!debouncedQuery) {setResults([]);return;}const controller = new AbortController();setIsLoading(true);fetcher(debouncedQuery).then((data) => {if (!controller.signal.aborted) setResults(data);}).finally(() => {if (!controller.signal.aborted) setIsLoading(false);});return () => controller.abort();}, [debouncedQuery, fetcher]);return { query, setQuery, results, isLoading };}
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 Bedeutungfunction useToggle(initial = false): [boolean, () => void] {const [value, setValue] = useState(initial);const toggle = useCallback(() => setValue((v) => !v), []);return [value, toggle];}const [isVisible, toggleVisibility] = useToggle(); // Klarconst { user, logout } = useAuth(); // Auch klar
Anti-Patterns
1. Der Gott-Hook
Ein Hook, der alles macht, ist nicht besser als eine Komponente, die alles macht:
tsx// Das nicht machenfunction useApp() {const [user, setUser] = useState(null);const [theme, setTheme] = useState("light");const [notifications, setNotifications] = useState([]);const [cart, setCart] = useState([]);const [isMenuOpen, setIsMenuOpen] = useState(false);// ... 200 weitere Zeilen}
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 Mehrwertfunction useIsOpen() {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 machenfunction useFormattedPrice(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:
tsx// Stattdessen das machenfunction useFormattedPrice(cents: number) {return useMemo(() => `$${(cents / 100).toFixed(2)}`, [cents]);}
Oder noch einfacher, das muss überhaupt kein Hook sein:
tsxfunction formatPrice(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:
tsx// Memory-Leak beim Unmountfunction useWindowSize() {const [size, setSize] = useState({ width: 0, height: 0 });useEffect(() => {const handler = () =>setSize({width: window.innerWidth,height: window.innerHeight,});window.addEventListener("resize", handler);// Fehlt: return () => window.removeEventListener('resize', handler);}, []);return size;}
Das leakt. Jedes Mal wenn die Komponente mountet, wird ein neuer Listener hinzugefügt und nie entfernt. Die Lösung:
tsxfunction useWindowSize() {const [size, setSize] = useState({width: typeof window !== "undefined" ? window.innerWidth : 0,height: typeof window !== "undefined" ? window.innerHeight : 0,});useEffect(() => {const handler = () =>setSize({width: window.innerWidth,height: window.innerHeight,});window.addEventListener("resize", handler);return () => window.removeEventListener("resize", handler);}, []);return size;}
Die Bereinigungs-Regel
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 Countfunction useCounter() {const [count, setCount] = useState(0);const showCount = useCallback(() => {setTimeout(() => alert(count), 1000);}, []); // 'count' fehlt in den Dependenciesreturn { 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:
tsx// Das nicht machenfunction useErrorBanner(error: string | null) {const banner = error ? <div className="error">{error}</div> : null;return { banner };}// Stattdessen - Daten zurückgeben, Komponente rendertfunction useError() {const [error, setError] = useState<string | null>(null);const clear = useCallback(() => setError(null), []);return { error, setError, clear };}
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.
eslint-plugin-react-hooks (Offiziell)
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:
json{"react-hooks/exhaustive-deps": ["warn",{"additionalHooks": "(useCustomEffect|useAnimationFrame)"}]}
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-functionerkennt Hooks, die zu gross geworden sind, um sie auf einen Blick zu verstehen. 80 Zeilen sind ein vernünftiger Startwert, genug für einenuseReducermit ein paar Handlern, eng genug um Hooks zu markieren, die fünf unzusammenhängende Belange verwalten.complexitymisst 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-statementsbegrenzt die Anzahl der Anweisungen. Wenn ein Hook 20const-Deklarationen hat, macht er fast sicher zu viel.max-deptherkennt 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:
eslint.config.jsimport reactHooks from "eslint-plugin-react-hooks";import useEncapsulation from "eslint-plugin-use-encapsulation";export default [{plugins: {"react-hooks": reactHooks,"use-encapsulation": useEncapsulation,},rules: {// Kern-Hook-Korrektheit"react-hooks/rules-of-hooks": "error","react-hooks/exhaustive-deps": "warn",// Kapselung durchsetzen"use-encapsulation/prefer-custom-hooks": ["warn"],// Gott-Hooks erkennen"max-lines-per-function": ["warn",{max: 80,skipBlankLines: true,skipComments: true,},],complexity: ["warn", 10],"max-statements": ["warn", 15],},},];
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:
tsxtype ValidationRule<T> = {validate: (value: T[keyof T], values: T) => boolean;message: string;};type FieldConfig<T> = {initialValue: T[keyof T];rules?: ValidationRule<T>[];};type FormConfig<T> = {[K in keyof T]: FieldConfig<T>;};function useForm<T extends Record<string, unknown>>(config: FormConfig<T>) {type Errors = Partial<Record<keyof T, string>>;// config wird als stabil erwartet (außerhalb von render definiert oder memoized).// initialValues einmalig ableiten vermeidet Neuberechnung.const [initialValues] = useState<T>(() => {const values = {} as T;for (const key in config) {values[key] = config[key].initialValue;}return values;});const [values, setValues] = useState<T>(initialValues);const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});const [isSubmitting, setIsSubmitting] = useState(false);const errors = useMemo<Errors>(() => {const result: Errors = {};for (const key in config) {const rules = config[key].rules ?? [];for (const rule of rules) {if (!rule.validate(values[key], values)) {result[key] = rule.message;break;}}}return result;}, [values, config]);const isValid = Object.keys(errors).length === 0;const setValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {setValues((prev) => ({ ...prev, [field]: value }));}, []);const setFieldTouched = useCallback((field: keyof T) => {setTouched((prev) => ({ ...prev, [field]: true }));}, []);const reset = useCallback(() => {setValues(initialValues);setTouched({});setIsSubmitting(false);}, [initialValues]);const handleSubmit = useCallback((onSubmit: (values: T) => Promise<void>) => async (e: FormEvent) => {e.preventDefault();setTouched(() => {const allTouched: Partial<Record<keyof T, boolean>> = {};for (const key in config) allTouched[key] = true;return allTouched;});setValues((currentValues) => {const currentErrors: Errors = {};for (const key in config) {for (const rule of config[key].rules ?? []) {if (!rule.validate(currentValues[key], currentValues)) {currentErrors[key] = rule.message;break;}}}if (Object.keys(currentErrors).length > 0) return currentValues;setIsSubmitting(true);onSubmit(currentValues).finally(() => setIsSubmitting(false));return currentValues;});},[config],);return {values,errors,touched,isValid,isSubmitting,setValue,setFieldTouched,reset,handleSubmit,};}
Verwendung:
tsxfunction SignupForm() {const form = useForm({email: {initialValue: "",rules: [{validate: (v) => typeof v === "string" && v.length > 0,message: "Pflichtfeld",},{validate: (v) => typeof v === "string" && v.includes("@"),message: "Ungueltige E-Mail",},],},password: {initialValue: "",rules: [{validate: (v) => typeof v === "string" && v.length >= 8,message: "Mindestens 8 Zeichen",},],},});return (<formonSubmit={form.handleSubmit(async (values) => {await api.signup(values.email, values.password);})}><inputvalue={form.values.email}onChange={(e) => form.setValue("email", e.target.value)}onBlur={() => form.setFieldTouched("email")}/>{form.touched.email && form.errors.email && (<span>{form.errors.email}</span>)}{/* ... Passwort-Feld, Submit-Button */}</form>);}
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.
Quellen & Weiterführende Links
- useEncapsulation: Kyle Shevlin
Der Original-Artikel, der den Begriff 'useEncapsulation' prägte und dafür argumentiert, alle React Hooks in Custom Hooks zu kapseln.
- Reusing Logic with Custom Hooks: React Docs
Die offizielle React-Dokumentation zu Custom Hooks: Wann extrahieren, Namenskonventionen und wie State-Isolation funktioniert.
- You Might Not Need an Effect: React Docs
Reacts offizieller Guide zum Vermeiden unnötiger useEffect-Aufrufe, eine der wirkungsvollsten Seiten in der Dokumentation.
- React Hooks Pitfalls: Kent C. Dodds
Fünf Tipps zum Vermeiden gängiger Hook-Fehler, einschließlich veralteter Closures und Dependency-Array-Problemen.
- eslint-plugin-use-encapsulation
Kyle Shevlins ESLint-Plugin, das das Custom-Hook-Kapselungs-Pattern mit einer prefer-custom-hooks-Regel durchsetzt.
- eslint-plugin-react-hooks: React
Die offizielle React ESLint-Plugin-Referenz, rules-of-hooks, exhaustive-deps und die neueren React Compiler Rules.
