Zum Inhalt springen

7.6 React-Grundlagen: Die Hook-Familie

Zu Zen-Modus wechseln

Nach dieser Einheit kannst du:

  • Seiteneffekte mit useEffect umsetzen, mit Dependency-Array und Cleanup, und die Endlosschleifen-Falle vermeiden.
  • useRef für DOM-Zugriff und unsichtbare Merkwerte einsetzen und von State abgrenzen.
  • die Einsatzgebiete von useContext, useMemo und useCallback nennen und in fremdem Code erkennen.
  • wiederverwendbare Zustandslogik in einen eigenen Hook auslagern.

Ein Werkzeugkoffer hat für jede Aufgabe ein passendes Werkzeug: den Hammer für Nägel, den Schraubenzieher für Schrauben, den Zollstock zum Messen. Wer alles mit dem Hammer erledigt, bekommt die Schraube auch irgendwie in die Wand, aber nicht gut. Die Hooks von React sind so ein Koffer: useState hast du in Einheit 7.4 kennengelernt, useRef in 7.5 kurz gesehen. Diese Einheit packt den Rest aus und sagt zu jedem Werkzeug, wofür es gedacht ist und wofür nicht.

flowchart TD
    accTitle: Die Hook-Familie und ihre Aufgaben
    hooks["Hooks"] --> s["useState<br/>sichtbarer Zustand"]
    hooks --> e["useEffect<br/>Kontakt zur Außenwelt"]
    hooks --> r["useRef<br/>DOM und unsichtbare Werte"]
    hooks --> c["useContext<br/>Werte ohne Prop-Treppe"]
    hooks --> m["useMemo, useCallback<br/>Optimierung"]
    hooks --> own["eigene Hooks<br/>wiederverwendbare Logik"]

Abb. 7.23: Jeder Hook hat eine Aufgabe; wer den passenden nicht benennen kann, fragt zuerst, ob es überhaupt Zustand ist (eigene Darstellung).

Für die ganze Familie gelten die zwei Regeln aus Einheit 7.4: Aufruf nur auf der obersten Ebene einer Komponente und nur in Komponenten oder in eigenen Hooks.

  • Seiteneffekt: alles, was beim Rendern über “Props und State hinein, JSX heraus” hinausgeht, etwa Daten laden oder einen Timer starten.
  • Dependency-Array: die Liste von Werten, bei deren Änderung ein Effekt erneut läuft.
  • Cleanup: die Aufräumfunktion eines Effekts, bevor er erneut läuft oder die Komponente verschwindet.
  • Prop Drilling: einen Wert durch viele Ebenen Props reichen, die ihn selbst nicht brauchen.
  • Memoisieren: ein Ergebnis aufheben und wiederverwenden, solange sich seine Eingaben nicht ändern.

Rendern soll rein sein: Props und State hinein, JSX heraus. Alles, was darüber hinausgeht (Daten laden, Timer starten, den Dokumenttitel setzen), ist ein Seiteneffekt und gehört in useEffect. Im Beispiel holt loadJob einen Auftrag vom Server, etwa per fetch wie in Kapitel 5:

function JobMonitor({ jobId }: { jobId: number }) {
const [job, setJob] = useState<Job | null>(null);
useEffect(() => {
let canceled = false;
loadJob(jobId).then((loaded) => {
if (!canceled) setJob(loaded); // ignore stale responses
});
return () => { canceled = true; }; // cleanup on unmount / re-run
}, [jobId]); // re-run when jobId changes
if (job === null) return <p>loading ...</p>;
return <JobCard filename={job.filename} progress={job.progress} />;
}

Beispielprojekt: 07_react_grundlagen_examples/ex19_job_monitor

Die drei Bestandteile:

  • die Effekt-Funktion: Sie läuft nach dem Rendern.
  • das Dependency-Array: Der Effekt läuft erneut, wenn sich einer der aufgezählten Werte ändert; [] bedeutet nur einmal nach dem ersten Rendern.
  • die optionale Cleanup-Funktion: Sie räumt auf, bevor der Effekt erneut läuft oder die Komponente verschwindet. Hier entschärft sie die Race Condition aus Kapitel 4: Wechselt jobId schneller, als der Server antwortet, wird die überholte Antwort ignoriert.
sequenceDiagram
    accTitle: Effekt und Cleanup bei wechselnder jobId
    participant comp as JobMonitor
    participant eff as Effekt
    comp->>eff: Rendern mit jobId 1, Effekt startet loadJob(1)
    comp->>eff: Rendern mit jobId 2
    eff-->>eff: Cleanup: canceled = true für Job 1
    eff->>eff: Effekt startet loadJob(2)
    Note over eff: Antwort zu Job 1 kommt spät und wird ignoriert

Abb. 7.24: Vor jedem neuen Lauf räumt der alte Effekt auf; überholte Antworten bleiben folgenlos (eigene Darstellung).

Im Entwicklungsmodus führt StrictMode jeden Effekt einmal zusätzlich aus und räumt dazwischen auf. Damit testet React, ob dein Cleanup wirklich alles rückgängig macht; im Build passiert das nicht.

Zwei Warnungen aus der Praxis. Erstens: Ein fehlendes Dependency-Array plus Setter im Effekt ergibt eine Endlosschleife aus Rendern und Effekt. Zweitens: useEffect ist kein Werkzeug für abgeleitete Werte. Was sich aus Props und State ausrechnen lässt (gefilterte Liste, Summe), wird direkt beim Rendern berechnet, ohne Effekt und ohne eigenen State.

Auflösung

(a) Endlosschleife: Der Effekt läuft nach jedem Rendern, der Setter löst ein neues Rendern aus, und so weiter. (b) Der Effekt läuft einmal nach dem ersten Rendern, count wird 1 und bleibt dort. (c) Ebenfalls eine Endlosschleife: Jede Änderung von count startet den Effekt neu, der count wieder ändert.

useRef liefert ein Objekt mit einer beschreibbaren .current-Eigenschaft, das über alle Renderdurchgänge dasselbe bleibt. Zwei Einsatzfälle:

  • der Zugriff auf DOM-Elemente, wie beim uncontrolled Feld in Einheit 7.5 oder für inputRef.current?.focus();
  • das Aufbewahren veränderlicher Werte, die die Anzeige nicht betreffen, etwa eine Timer-ID oder ein Merker gegen Doppelklicks.

Eine Änderung an .current löst kein Rendern aus; genau das unterscheidet die Ref vom State.

Manche Werte braucht fast jede Komponente: das Farbschema, die angemeldete Benutzerin. Sie durch fünf Ebenen Props zu reichen (Prop Drilling) macht alle Zwischenkomponenten zu Durchreichern:

// theme is only needed in ThemeBadge, but every level has to pass it along
<App theme="dark">
<Page theme={theme}> {/* just forwards */}
<Sidebar theme={theme}> {/* just forwards */}
<ThemeBadge theme={theme} /> {/* finally uses it */}

Das Problem ist nicht die Menge Code, sondern die Kopplung: Page und Sidebar müssen von einem Prop wissen, das sie nichts angeht, und jede neue Zwischenebene muss es ebenfalls durchschleifen. Ein Context stellt einen Wert allen Nachkommen direkt zur Verfügung:

const ThemeContext = createContext<"light" | "dark">("light");
function App() {
return (
<ThemeContext value="dark">
<Toolbar /> {/* anywhere below ... */}
</ThemeContext>
);
}
function ThemeBadge() {
const theme = useContext(ThemeContext); // ... read it directly
return <span>{theme}</span>;
}

Beispielprojekt: 07_react_grundlagen_examples/ex20_theme_context

In älterem Code steht statt <ThemeContext value="dark"> noch <ThemeContext.Provider value="dark">; das ist die Schreibweise vor React 19 und funktioniert weiterhin.

flowchart LR
    accTitle: Prop Drilling gegen Context
    subgraph drill["Prop Drilling"]
        direction TB
        a1["App"] -- "theme" --> p1["Page"]
        p1 -- "theme" --> s1["Sidebar"]
        s1 -- "theme" --> t1["ThemeBadge"]
    end
    subgraph ctx["Context"]
        direction TB
        a2["App<br/>ThemeContext"] --> p2["Page"]
        p2 --> s2["Sidebar"]
        s2 --> t2["ThemeBadge"]
        a2 -. "useContext" .-> t2
    end

Abb. 7.25: Mit Context wissen die Zwischenebenen nichts vom Wert (eigene Darstellung).

Context ist für querschnittliche, selten wechselnde Werte gedacht. Er ist kein Ersatz für gewöhnliche Props: Wer jeden Zustand global macht, verliert die Nachvollziehbarkeit des Datenflusses.

Wo begegnet dir das? Beim Dunkelmodus vieler Webanwendungen: Ein einziger Schalter in den Einstellungen färbt Kopfzeile, Menüs, Karten und Dialoge um, ohne dass jede Komponente dazwischen davon wissen muss.

Jedes Mal, wenn React eine Komponente neu rendert, läuft ihre Funktion komplett von oben durch. Meistens ist das gleichgültig, weil React schnell ist. Es wird erst zum Problem, wenn eine Berechnung wirklich teuer ist (etwa das Filtern und Sortieren tausender Einträge bei jedem Tastendruck) oder wenn eine im Rumpf erzeugte Funktion als Prop an eine Kindkomponente geht, die nur bei echten Änderungen neu rendern soll. Für diese beiden Fälle gibt es je ein Werkzeug, das ein Ergebnis über mehrere Renderdurchgänge aufhebt (memoisiert):

  • useMemo für die teure Berechnung: Es merkt sich das Ergebnis der Funktion, bis sich eine Abhängigkeit ändert.
  • useCallback für die stabile Funktion: Es merkt sich die Funktion selbst, sodass Kindkomponenten bei jedem Rendern dieselbe Referenz sehen.
// useMemo: skip the expensive computation unless `jobs` changed
const failedJobs = useMemo(() => jobs.filter((j) => j.state === "failed"), [jobs]);
// useCallback: hand the child the same function reference across renders
const handleCancel = useCallback(
(id: number) => setJobs((current) => current.filter((j) => j.id !== id)),
[],
);

Beispielprojekt: 07_react_grundlagen_examples/ex21_memo_callback

Beide bekommen eine Funktion und ein Dependency-Array; der Unterschied ist nur, ob das Aufgehobene das Ergebnis der Funktion ist (useMemo) oder die Funktion selbst (useCallback). Technisch ist useCallback(fn, deps) nichts anderes als die Kurzform für useMemo(() => fn, deps). Das leere Array bei useCallback ist hier erlaubt, weil die Funktionsform des Setters den aktuellen Stand selbst bekommt.

Wichtig bleibt die Reihenfolge: Beide sind Optimierungen, und der Standardfall ist, sie nicht zu brauchen. Du greifst zu ihnen, wenn ein konkreter Engpass messbar ist, nicht vorsorglich; vorschnelle Memoisierung macht Code komplizierter statt schneller. Ins Repertoire gehören sie trotzdem, damit du sie in fremdem Code liest.

Wiederkehrende Zustandslogik lässt sich in eigene Hooks auslagern: gewöhnliche Funktionen mit use-Präfix, die andere Hooks verwenden. Ein Klassiker, der Eingaben über ein Neuladen der Seite hinweg erhält:

function useLocalStorage(key: string, initial: string) {
const [value, setValue] = useState(
() => localStorage.getItem(key) ?? initial);
useEffect(() => {
localStorage.setItem(key, value);
}, [key, value]);
return [value, setValue] as const;
}
// in any component, same shape as useState:
const [draft, setDraft] = useLocalStorage("comment-draft", "");

Beispielprojekt: 07_react_grundlagen_examples/ex22_use_local_storage

Drei Details: useState(() => ...) ist die Lazy-Form, bei der React die Funktion nur beim ersten Rendern aufruft, statt bei jedem Durchgang aus dem Speicher zu lesen. localStorage speichert nur Text, deshalb arbeitet der Hook mit Strings. Und as const sorgt dafür, dass TypeScript das Ergebnis als Paar [string, Setter] kennt statt als Array gemischter Typen.

flowchart LR
    accTitle: Ein eigener Hook kapselt State und Effekt
    comp["Komponente<br/>useLocalStorage(...)"] --> hook["useLocalStorage"]
    hook --> st["useState<br/>Wert"]
    hook --> ef["useEffect<br/>speichern"]
    ef --> ls[("localStorage")]
    ls -. "beim ersten Rendern lesen" .-> st

Abb. 7.26: Die Komponente sieht nur ein Paar aus Wert und Setter; Laden und Speichern sind im Hook versteckt (eigene Darstellung).

Eigene Hooks sind Reacts Antwort auf die Frage der Wiederverwendung von Logik, so wie Komponenten Oberfläche wiederverwenden. Ein guter eigener Hook kennt die Anwendung nicht: useLocalStorage funktioniert für einen Kommentarentwurf genauso wie für einen Bestwert im Quiz.

Auflösung

(a) useRef für das Feld und inputRef.current?.focus() in einem useEffect. (b) useEffect mit [] und Cleanup, das Ergebnis landet über useState in der Anzeige. (c) useContext, ein querschnittlicher, selten wechselnder Wert. (d) Gar kein Hook: Die Zahl wird beim Rendern aus der Liste der markierten Aufträge berechnet.

  1. Effekt ohne Dependency-Array, mit Setter darin: Endlosschleife aus Rendern und Effekt.
  2. useEffect für abgeleitete Werte: eine gefilterte Liste in einem Effekt in einen zweiten State schreiben, statt sie beim Rendern zu berechnen.
  3. Cleanup vergessen: Timer laufen weiter, überholte Antworten überschreiben neuere.
  4. Ref für Sichtbares: Eine Änderung an ref.current erscheint nicht auf der Seite, weil sie kein Rendern auslöst.
  5. Alles in den Context: Jeder Zustand wird global, der Datenfluss ist nicht mehr nachvollziehbar.
  6. Vorsorglich memoisiert: useMemo um jede Kleinigkeit macht den Code komplizierter, ohne messbaren Gewinn.

Du kannst dir diese Einheit kostenlos mit der Vorlesefunktion deines Browsers oder Betriebssystems vorlesen lassen (etwa „Laut vorlesen” in Edge), ohne Konto und ohne Upload; die Codebeispiele liest du dabei am Bildschirm mit. Freiwillig kannst du mit Google NotebookLM (Google-Konto nötig) aus diesem öffentlichen Text eine gesprochene Zusammenfassung erzeugen, auch auf Deutsch.

Du kannst jetzt:

  • Seiteneffekte mit useEffect umsetzen, mit Dependency-Array und Cleanup, und die Endlosschleifen-Falle vermeiden.
  • useRef für DOM-Zugriff und unsichtbare Merkwerte einsetzen und von State abgrenzen.
  • die Einsatzgebiete von useContext, useMemo und useCallback nennen und in fremdem Code erkennen.
  • wiederverwendbare Zustandslogik in einen eigenen Hook auslagern.
  • Aufgabe 07-6a - Quiz reloaded, alle Stufen
  • Oliver Maurhart (MAO)
  • Michael Gabriel (GAM)

CC-BY-SA

https://qiz.at/rules