Custom Hooks sind ohne strenge Regeln und ohne erzwungene Struktur — und genau das macht Konventionen wertvoll. Die React-Community hat in mehreren Jahren einen relativ stabilen Konsens entwickelt, was als sauber gilt und was als Code-Smell. Dieser Artikel sammelt die wichtigsten Naming-Konventionen (warum use-Prefix Pflicht ist, wie der Rest des Namens die Domain widerspiegeln sollte), Best Practices für Rückgabe-Form, Parameter-Reihenfolge und Datei-Organisation — und benennt die typischen Anti-Patterns, die in CodeReviews regelmäßig auffallen. Wer diese Konventionen einhält, schreibt Hooks, die andere Entwickler sofort verstehen und sicher einsetzen können.
Der use-Prefix ist Pflicht
Jeder Custom Hook beginnt mit use. Das ist keine bloße Konvention — es ist eine technische Notwendigkeit: der Linter eslint-plugin-react-hooks nutzt das Prefix, um zwischen Hooks und normalen Funktionen zu unterscheiden. Nur Funktionen mit use-Prefix dürfen andere Hooks aufrufen, und nur sie werden auf Hook-Regeln geprüft.
// FALSCH — Hook-Aufruf in Funktion ohne `use`-Prefix
function counter() {
const [c, setC] = useState(0); // ESLint: "React Hook is called in a function"
return c;
}
// RICHTIG — `use`-Prefix macht es zum Hook
function useCounter() {
const [c, setC] = useState(0);
return c;
}Konventionell wird der Rest des Namens als camelCase geschrieben und beschreibt was der Hook tut oder zurückgibt: useUser, useFormValidation, useScrollPosition, useOnlineStatus. Vermeiden:
- Generische Namen wie
useData,useHelper,useUtil— sie sagen nichts aus. - Verb-fokussierte Namen wie
useFetchData— der Hook ist kein Verb, er liefert State/Funktionen.useApioderuseUserListist konkreter. - Großbuchstabe nach
usewieUse— das wäre eine Komponente.
Sprechende Namen entlang der Domain
Ein guter Hook-Name beschreibt das Verhalten aus Sicht der Konsumenten, nicht die interne Implementierung.
| ❌ Implementierung im Namen | ✅ Verhalten im Namen | Was er tut |
|---|---|---|
useFetchAndState | useUser | Lädt einen User und liefert State + Loading |
useEffectListener | useOnlineStatus | Beobachtet navigator.onLine-Änderungen |
useStateAndLocalStorage | useLocalStorage | State, der mit localStorage synchronisiert |
useCallbackWrapper | useDebounce | Verzögert einen Wert um N Millisekunden |
useStateForForm | useFormField | Verwaltet einen einzelnen Form-Feld-Wert |
Der Konsument denkt in Bedeutungen, nicht in Hooks-Internals. useOnlineStatus macht beim Lesen sofort klar, was zurückkommt — useEffectListener lässt offen, wofür.
Rückgabe-Form: Tupel vs. Object
Zwei verbreitete Rückgabe-Formen, mit klaren Anwendungsfällen.
Tupel — passt für genau zwei Werte mit „klassischer" Reihenfolge wie bei useState selbst.
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(v => !v);
return [value, toggle]; // Tupel: [state, action]
}
// Konsument benennt frei:
const [isOpen, toggleOpen] = useToggle();
const [isDark, toggleDark] = useToggle(true);Vorteil: Konsumenten benennen frei. Nachteil: bei drei oder mehr Werten wird die Reihenfolge zur impliziten API, die schwer zu merken ist.
Object — passt für drei oder mehr Werte, oder wenn man semantisch klar machen will, was die einzelnen Teile bedeuten.
function useUser(id) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const refetch = () => { /* … */ };
return { user, isLoading, error, refetch }; // Object: benannt
}
// Konsument destructured nur, was er braucht:
const { user, isLoading } = useUser(42);Vorteil: explizit, ergänzbar (neue Felder brechen bestehende Konsumenten nicht), partielles Destructuring. Nachteil: keine freie Umbenennung — wer einen anderen Namen will, muss aliasen (const { user: currentUser } = useUser(42)).
Faustregel: Tupel ab useState-artigen zwei Werten; Object ab drei. Mische die Formen nicht innerhalb einer Codebase — Konsistenz hilft Konsumenten.
Parameter-Reihenfolge: erforderlich, dann optional
Bei mehreren Parametern: erforderliche zuerst, optionale (mit Default) am Ende. Bei vielen Optionen lieber ein Options-Object übergeben — keine 5er-Argumentliste.
// ✅ Klein und klar: positionale Parameter mit Defaults
function useDebounce(value, delay = 300) {
// …
}
const debouncedQuery = useDebounce(query, 500);
// ❌ Zu viele positionale Parameter — Reihenfolge schwer zu merken
function useFetch(url, method, headers, retries, timeout, signal) {
// …
}
useFetch('/api', 'GET', {}, 3, 5000, signal); // Was ist 3? Was ist 5000?
// ✅ Options-Object — explizit, ergänzbar
function useFetch(url, options = {}) {
const {
method = 'GET',
headers = {},
retries = 3,
timeout = 5000,
signal,
} = options;
// …
}
useFetch('/api', { retries: 5, timeout: 10_000 });Daumenregel: ab dem dritten Konfigurations-Parameter zum Options-Object wechseln. Macht den Aufruf selbst-dokumentierend.
Stabilität von zurückgegebenen Funktionen
Funktionen, die ein Custom Hook zurückgibt, werden bei jedem Render neu erzeugt — es sei denn, sie sind mit useCallback stabilisiert. Das wirkt sich aus, wenn Konsumenten:
- die Funktion an einen
React.memo-Wrapper als Prop weitergeben (instabile Funktion → unnötiger Re-Render des Kindes), - die Funktion als
useEffect-Dependency setzen (instabile Funktion → Endlos-Loop oder falsche Wiederholungen), - die Funktion mit
useMemo/useCallbackweiter wrappen.
import { useState, useCallback } from 'react';
// ✅ Stabile Setter-Funktionen via useCallback
export function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
// useCallback hält die Funktions-Referenz zwischen Renders stabil
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 };
}Wann es egal ist: Wenn der Konsument die Funktion nur direkt in JSX als Event-Handler nutzt (<button onClick={inc}>), spielt Stabilität keine Rolle — der DOM-Handler kümmert sich nicht um Referenz-Identität. Wann es wichtig ist: API-mäßige Custom Hooks, die in Library-artigem Kontext genutzt werden, sollten stabile Funktionen liefern.
Hinweis: Der React Compiler (Stage 1 in React 19.x) automatisiert solche Memoizations. In Codebases mit aktivem Compiler darf man useCallback weglassen — der Compiler ergänzt es selbst, wo nötig.
Datei-Organisation
Best Practices, die in den meisten Codebases gut funktionieren:
- Ein Hook pro Datei.
useUser.ts,useDebounce.ts,useLocalStorage.ts. Klarer Mental Map zwischen Symbol und Datei. - Named Export, nicht Default Export. Named Exports unterstützen besseres IDE-Auto-Rename, sind in Tools wie Vite/esbuild Tree-Shaking-freundlicher und vermeiden Import-Namens-Drift (
import a from './useUser'würde funktionieren, aber die Indirektion ist Bug-anfällig). - Ordner-Struktur: ein
hooks/-Verzeichnis im Projekt-Root oder feature-spezifische Hooks im jeweiligen Feature-Ordner. Bei großen Projekten beides kombinieren: domain-übergreifende Hooks insrc/hooks/, feature-spezifische insrc/features/<feature>/hooks/. - Index-Datei für Sammel-Exports (optional).
hooks/index.tsmitexport * from './useUser'; export * from './useDebounce';erlaubtimport { useUser } from '~/hooks'. Vorteil: nur ein Import-Pfad. Nachteil: ggf. ungewollte Bundle-Größe, wenn Tree-Shaking nicht greift.
import { useState, useEffect } from 'react';
// Named Export — kein Default
export function useUser(id: number) {
// …
}Typische Anti-Patterns
Anti-Pattern 1: Hook ohne Hook-Aufruf.
// ❌ Keine Hook-Aufrufe — sollte normale Funktion sein
function useFormatPrice(amount) {
return `${amount.toFixed(2)} €`;
}
// ✅ Normale Utility-Funktion
function formatPrice(amount) {
return `${amount.toFixed(2)} €`;
}Wenn die Funktion keine Hooks intern aufruft, ist sie kein Custom Hook — sie ist eine Helper. Der use-Prefix verwirrt Konsumenten („darf ich das nur in Komponenten aufrufen?") und blockiert Linter-Regeln, die für echte Hooks gedacht sind.
Anti-Pattern 2: Hook ruft Side-Effects ohne Cleanup.
// ❌ setInterval ohne clearInterval beim Unmount — Memory-Leak
function useTimer(intervalMs) {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => setCount(c => c + 1), intervalMs);
// FEHLT: return () => clearInterval(...)
}, [intervalMs]);
return count;
}
// ✅ Mit Cleanup
function useTimer(intervalMs) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), intervalMs);
return () => clearInterval(id); // Cleanup beim Unmount
}, [intervalMs]);
return count;
}Anti-Pattern 3: Stale Closure in zurückgegebenen Funktionen.
// ❌ increment liest ALTEN count-Wert — Closure-Falle bei mehreren Calls
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
}
// ✅ Funktionaler Setter — immer aktueller Wert
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(c => c + 1);
return { count, increment };
}Anti-Pattern 4: Hook gibt JSX zurück.
// ❌ Hook rendert JSX — gehört in eine Komponente
function useTooltip(text) {
const [visible, setVisible] = useState(false);
return <div>{visible && text}</div>; // FALSCH
}
// ✅ Hook liefert State + Steuerung, Komponente rendert JSX
function useTooltip() {
const [visible, setVisible] = useState(false);
return { visible, show: () => setVisible(true), hide: () => setVisible(false) };
}Hooks geben Werte zurück, keine JSX-Elemente. Wer JSX braucht, schreibt eine Komponente. Der Hook regelt nur das Verhalten.
Anti-Pattern 5: Bedingter Hook-Aufruf im Inneren.
// ❌ Conditional Hook-Aufruf — Verstoß gegen Hook-Regeln
function useAuth(enabled) {
if (enabled) {
const user = useUser(); // HOOK-REGEL VERLETZT
return user;
}
return null;
}
// ✅ Hook IMMER aufrufen, Conditional Logik im Hook-Body
function useAuth(enabled) {
const user = useUser();
return enabled ? user : null;
}Häufige Stolperfallen
use-Prefix ist die einzige zwingende Konvention.
ESLint eslint-plugin-react-hooks erkennt am Prefix, dass die Funktion Hooks aufruft, und wendet die Hook-Regeln an. Funktionen ohne use-Prefix mit Hook-Aufruf werden als Verstoß markiert.
Generische Namen wie useData oder `useHelper` sind Code-Smell.
Konsumenten müssen den Code lesen, um zu verstehen, was kommt. useUser, useFormValidation, useOnlineStatus sind selbst-dokumentierend.
Rückgabe-Form bewusst wählen: Tupel für zwei Werte, Object ab drei.
Tupel erlauben freie Umbenennung beim Destructuring. Object ist explizit und ergänzbar. Konsistenz innerhalb einer Codebase hilft den Konsumenten.
Ab dem dritten Parameter zum Options-Object wechseln.
Lange positionale Argument-Listen (useFetch('url', 'GET', {}, 3, 5000)) sind schwer zu lesen und Bug-anfällig. useFetch('url', { method, retries, timeout }) ist selbst-erklärend.
Zurückgegebene Funktionen mit useCallback stabilisieren.
Bei API-mäßigen Hooks, deren Output in Effect-Dependencies oder als Memo-Prop landet. Bei reinem Event-Handler-Einsatz im JSX überflüssig.
Side-Effects in Hooks brauchen Cleanup.
setInterval, addEventListener, fetch mit AbortController — Cleanup-Funktion im useEffect-Return ist Pflicht, sonst Memory-Leak und Cross-Component-Trigger.
Hook ohne Hook-Aufruf ist keine Hook — sondern Utility-Funktion.
Wenn eine Funktion mit use-Prefix intern useState, useEffect usw. nicht nutzt, sollte sie umbenannt werden. formatPrice() statt useFormatPrice().
Hooks geben Werte zurück — niemals JSX.
Wer JSX braucht, schreibt eine Komponente. Der use-Prefix signalisiert „Logik-Funktion", der Großbuchstabe „Render-Komponente". Beides nicht mischen.
Weiterführende Ressourcen
Externe Quellen
- Reusing Logic with Custom Hooks – react.dev
- Rules of Hooks – react.dev
- eslint-plugin-react-hooks – npm
- React Compiler – react.dev