Ein React custom hook ist der offizielle Weg, wiederkehrende Logik aus deinen Komponenten in eine einzige wiederverwendbare Funktion auszulagern. Wenn dir auffällt, dass du dieselbe Kombination aus useState + useEffect in drei getrennte Komponenten kopiert hast, ist genau das der Moment, sie in einen Hook auszulagern. In diesem Leitfaden erkläre ich anhand echter Beispiele, was ein custom hook ist, wie man einen schreibt und wann er sich tatsächlich auszahlt.
Was ein custom hook ist – und was nicht
Ein custom hook ist eine ganz normale JavaScript-Funktion, deren Name mit use beginnt und die in ihrem Inneren andere Hooks aufrufen darf (useState, useEffect, useRef, sogar weitere custom hooks). Er hat keine magische Eigenschaft; React erwartet schlicht, dass jede Funktion mit dem Präfix use die Regeln der Hooks befolgt.
- Er teilt Logik, nicht State. Verwenden zwei Komponenten denselben Hook, erhält jede ihren eigenen unabhängigen State.
- Er gibt Daten, Funktionen oder eine Kombination davon zurück – niemals JSX.
- Er ist keine Komponente; er rendert nicht, er wird nur aus einer Komponente (oder einem anderen Hook) aufgerufen.
Die klarste Unterscheidung lautet: Ist dein Grund zum Auslagern „die UI wiederverwenden", extrahiere eine Komponente; ist er „die Logik wiederverwenden", extrahiere einen Hook.
Die zwei Regeln: wo man sie aufrufen darf
Hooks, ob custom oder eingebaut, gehorchen zwei Regeln, und sie zu verletzen macht deine App unvorhersehbar:
- Rufe sie nur auf der obersten Ebene auf. Rufe einen Hook niemals innerhalb einer Schleife, Bedingung oder verschachtelten Funktion auf. So kann sich React darauf verlassen, dass Hooks bei jedem Render in derselben Reihenfolge ausgeführt werden.
- Rufe sie nur aus React-Funktionen auf. Das heißt aus Komponenten oder aus anderen custom hooks. Rufe keinen Hook aus einer gewöhnlichen Hilfsfunktion auf.
Um diese Regeln automatisch durchzusetzen, installiere eslint-plugin-react-hooks; es fängt Fehlanwendungen ab, während du noch tippst.
Unser erster custom hook: useToggle
Beginnen wir mit etwas Einfachem, das du dennoch ständig brauchst. Dropdowns, Modals, „mehr anzeigen" – überall musst du einen Boolean umkehren:
import { useState, useCallback } from 'react';
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
Die Verwendung in einer Komponente ist herrlich sauber:
function Sidebar() {
const { value: isOpen, toggle, setFalse } = useToggle();
return (
<>
<button onClick={toggle}>Menü</button>
{isOpen && <nav onMouseLeave={setFalse}>...</nav>}
</>
);
}
Wir verpacken die zurückgegebenen Funktionen in useCallback, damit sie nicht bei jedem Render als neue Referenz erzeugt werden; so kannst du sie gefahrlos als useEffect-Abhängigkeiten verwenden.
Ein Hook mit Seiteneffekten: useLocalStorage
Die wahre Stärke zeigt sich, sobald useEffect und Browser-APIs ins Spiel kommen. Ein Hook, der einen Wert mit localStorage synchron hält, bündelt Logik, die sonst über Dutzende Komponenten verteilt wiederholt würde, an einer einzigen Stelle:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = window.localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch {
// Kontingent überschritten oder privater Modus: still ignorieren
}
}, [key, value]);
return [value, setValue];
}
Zwei Details sind hier wichtig. Erstens haben wir useState eine Funktion übergeben (einen Lazy Initializer), damit das Lesen aus localStorage nur beim ersten Render läuft, nicht bei jedem. Zweitens haben wir sowohl JSON.parse als auch den Zugriff auf localStorage in ein try/catch gehüllt, denn beschädigte Daten oder ein privater Tab, der den Zugriff verweigert, sind in Produktion eine sehr reale Fehlerquelle.
Ein Daten-Hook mit Aufräumen
Der häufigste Fehler beim asynchronen Laden ist der Versuch, State zu aktualisieren, nachdem die Komponente ausgehängt (unmount) wurde. AbortController löst das sauber:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [state, setState] = useState({ data: null, error: null, loading: true });
useEffect(() => {
const controller = new AbortController();
setState({ data: null, error: null, loading: true });
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => setState({ data, error: null, loading: false }))
.catch(error => {
if (error.name !== 'AbortError') {
setState({ data: null, error, loading: false });
}
});
return () => controller.abort();
}, [url]);
return state;
}
Die vom Effect zurückgegebene Aufräumfunktion bricht die vorherige Anfrage ab, sobald sich url ändert oder die Komponente ausgehängt wird. So vermeidest du sowohl Race Conditions als auch die bekannte Warnung „Can't perform a React state update on an unmounted component".
Wann einen Hook auslagern und wann warten
Verfrühte Abstraktion ist eine echte Falle. Ein praktischer Satz an Regeln:
- Lagere einen Hook aus, sobald du dieselbe Logik an mindestens zwei Stellen verwendest (oder sicher bist, dass du es tun wirst).
- Wird die Effect- und State-Logik innerhalb einer Komponente lang genug, um die Lesbarkeit zu beeinträchtigen, lohnt sich das Auslagern allein schon wegen der Testbarkeit.
- Ein
useFetchzu schreiben ist lehrreich, aber wenn du in Produktion Caching, Wiederholungsversuche und Invalidierung brauchst, ist eine ausgereifte Bibliothek wie TanStack Query meist die bessere Wahl. - Gestalte die Form, die dein Hook zurückgibt, als klaren Vertrag: entscheide bewusst, ob du ein Array zurückgibst (positionsbasiert, wie
useState) oder ein Objekt (benannt, bei mehreren Feldern).
Häufige Fragen
Was ist der Unterschied zwischen einem custom hook und einer normalen Funktion?
Ein custom hook darf in seinem Inneren andere Hooks wie useState und useEffect aufrufen und klinkt sich in Reacts Render-Lebenszyklus ein; eine gewöhnliche Hilfsfunktion kann das nicht. In der Praxis wird die Unterscheidung durch das Präfix use signalisiert, und der Linter setzt seine Regeln anhand dieses Präfixes durch.
Teilen sich zwei Komponenten den State, wenn sie denselben Hook verwenden?
Nein. Der Hook teilt nur die Logik; jede Komponente erhält ihren eigenen isolierten State. Möchtest du den State wirklich teilen, brauchst du Context oder eine State-Management-Lösung.
Wie teste ich custom hooks?
Mit dem Helfer renderHook aus React Testing Library kannst du den Hook isoliert rendern und seine Rückgabewerte sowie innerhalb von act ausgelöste Aktualisierungen prüfen. Enthält der Hook reine Logik, kannst du diese auch in eine separate Funktion auslagern und direkt testen.
Möchtest du deine wiederkehrende React-Logik in saubere Hooks überführen? Lass uns gemeinsam an deiner Frontend-Architektur und deinen React-Projekten arbeiten – nimm Kontakt mit mir auf.