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.

TypeScript UsePrefixWichtig.js
// 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. useApi oder useUserList ist konkreter.
  • Großbuchstabe nach use wie Use — 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 NamenWas er tut
useFetchAndStateuseUserLädt einen User und liefert State + Loading
useEffectListeneruseOnlineStatusBeobachtet navigator.onLine-Änderungen
useStateAndLocalStorageuseLocalStorageState, der mit localStorage synchronisiert
useCallbackWrapperuseDebounceVerzögert einen Wert um N Millisekunden
useStateForFormuseFormFieldVerwaltet 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.

TypeScript TupelRueckgabe.js
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.

TypeScript ObjectRueckgabe.js
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.

TypeScript ParameterReihenfolge.js
// ✅ 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/useCallback weiter wrappen.
TypeScript StabileRueckgabe.js
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 in src/hooks/, feature-spezifische in src/features/<feature>/hooks/.
  • Index-Datei für Sammel-Exports (optional). hooks/index.ts mit export * from './useUser'; export * from './useDebounce'; erlaubt import { useUser } from '~/hooks'. Vorteil: nur ein Import-Pfad. Nachteil: ggf. ungewollte Bundle-Größe, wenn Tree-Shaking nicht greift.
TypeScript hooks/useUser.ts
import { useState, useEffect } from 'react';

// Named Export — kein Default
export function useUser(id: number) {
    // …
}

Typische Anti-Patterns

Anti-Pattern 1: Hook ohne Hook-Aufruf.

TypeScript AntiPattern1.js
// ❌ 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.

TypeScript AntiPattern2.js
// ❌ 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.

TypeScript AntiPattern3.js
// ❌ 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.

TypeScript AntiPattern4.js
// ❌ 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.

TypeScript AntiPattern5.js
// ❌ 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

/ Weiter

Zurück zu Custom Hooks

Zur Übersicht