Startseite / Artikel / Ein wiederverwendbares Toolkit für benutzerdefinierte Hooks für jedes neue React-Projekt

Ein wiederverwendbares Toolkit für benutzerdefinierte Hooks für jedes neue React-Projekt

Entdecken Sie eine sorgfältig ausgewählte Sammlung an benutzerdefinierten React-Hooks – die Themen Speicherung, Verzögerung, Klicks und Datenabruf abdecken – die wiederholenden Standardcodeabschnitte in neuen Projekten beseitigen.

3840 Wörter

React-Hooks sind zur Standardmethode geworden, um Zustand, Nebeneffekte sowie wiederverwendbare Logik in modernen Anwendungen zu verwalten, doch das Kennen der eingebauten Hooks ist nur die halbe Miete. Ebenso wertvoll ist es, ein eigenes Werkzeugset aus kleinen, gut getesteten benutzerdefinierten Hooks zu haben, die man in jedes neue Projekt einbinden kann, um derselben Funktionalitäten nicht von Grund auf neu erstellen zu müssen. In diesem Artikel werden zunächst einige Hooks vorgestellt, die man in seinem Starter-Kit haben sollte, anschließend wird genauer erklärt, wie die zugrunde liegenden Hook-APIs tatsächlich funktionieren, damit man nicht nur versteht, was kopiert werden soll, sondern auch warum sie sich auf diese Weise verhalten.

Beim Starten eines neuen React-Projekts tauchen bestimmte Hooks fast jedes Mal wieder auf. Einige beheben Leistungsprobleme, andere verbessern die Benutzererfahrung, und wieder andere verhindern einfach, dass man dieselbe Standardlogik in jedem Codebase neu schreiben muss. Im Laufe mehrerer Projekte werden diese Hilfsmittel zu einem Standard-Startset: Anstatt das gleiche Problem von Grund auf zu lösen, verwendet man den Hook, passt ihn an, falls das Projekt etwas anderes benötigt, und konzentriert sich wieder auf die Entwicklung eigentlicher Funktionen. Im Folgenden sind zwölf Hooks, die dieser Beschreibung entsprechen.

Der erste ist useLocalStorage; er ist nützlich, weil fast jede Anwendung etwas über mehrere Neuladungen hinweg speichern muss – sei es eine Themenvorliebe, Eingabedaten eines Formulars oder andere Benutzereinstellungen.

import { useState } from "react";
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });  const updateValue = (newValue) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };  return [value, updateValue];
}

Dieses Muster wird häufig verwendet, um Dinge wie die Vorliebe für Dark Mode, ein Authentifizierungstoken oder jegliche Einstellung zu speichern, die zwischen den Besuchen gespeichert bleiben soll.

Dann gibt es useToggle, für den häufigen Fall, in dem ein Zustand einfach zwischen true und false wechselt.

import { useState } from "react";
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);  const toggle = () => setValue(v => !v);  return [value, toggle];
}

Es eignet sich ideal für Modale, Dropdown-Menüs, Seitenleisten und ähnliche per Schalter steuerbare UI-Elemente.

useDebounce wird ständig bei Sucheingaben verwendet, wo man keine Anfrage bei jeder Tastenbetätigung auslösen möchte.

import { useState, useEffect } from "react";
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounced(value);
    }, delay);    return () => clearTimeout(timer);
  }, [value, delay]);  return debounced;
}

Durch das Aufschieben der Aktualisierung, bis eine Tipppause eintritt, werden überflüssige API-Aufrufe reduziert.

useWindowSize gibt responsiven Layouts den Zugriff auf die aktuellen Abmessungen des Ansichtsfensters.

import { useState, useEffect } from "react";
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });  useEffect(() => {
    const resize = () =>
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });    window.addEventListener("resize", resize);    return () => window.removeEventListener("resize", resize);
  }, []);  return size;
}

usePrevious ist nützlich, wenn Sie einen Wert mit seinem Zustand bei der letzten Darstellung vergleichen müssen.

import { useEffect, useRef } from "react";
function usePrevious(value) {
  const ref = useRef();  useEffect(() => {
    ref.current = value;
  }, [value]);  return ref.current;
}

Das ist besonders nützlich für Animationen oder um festzustellen, ob sich etwas tatsächlich geändert hat.

useClickOutside schließt UI-Elemente wie Modale, wenn der Benutzer irgendwo außerhalb davon klickt – das entspricht dem intuitiven Erwartungsverhalten der Benutzer bezüglich solcher Komponenten.

import { useEffect } from "react";
function useClickOutside(ref, callback) {
  useEffect(() => {
    function handleClick(e) {
      if (ref.current && !ref.current.contains(e.target)) {
        callback();
      }
    }    document.addEventListener("mousedown", handleClick);    return () =>
      document.removeEventListener("mousedown", handleClick);
  }, [ref, callback]);
}

Es funktioniert gut für Dropdowns, Popovers und mobile Navigationsmenüs.

useDocumentTitle hält den Tab-Titel im Browser in Einklang mit dem aktuellen Anzeigebereich, was die Navigation und Orientierung erleichtert.

import { useEffect } from "react";
function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

Anstatt denselben Effekt in vielen Komponenten zu duplizieren, rufen Sie diesen Hook dort auf, wo eine Seite einen benutzerdefinierten Titel benötigt.

useFetch umhüllt das grundlegende Datenabrufen in einen wiederverwendbaren Hook.

import { useState, useEffect } from "react";
function useFetch(url) {
  const [data, setData] = useState(null);  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData);
  }, [url]);  return data;
}

Für Projekte, die größer sind als kleine Anwendungen, eignen sich Tools wie React Query oder SWR in der Regel besser, doch diese leichte Version reicht aus, um eine kleine App zum Laufen zu bringen.

useCopyToClipboard kümmert sich um die mittlerweile allgegenwärtigen Kopieren-Buttons.

function useCopyToClipboard() {
  const copy = (text) => {
    navigator.clipboard.writeText(text);
  };
  return copy;
}

Es eignet sich hervorragend, um Einladungslinks, Gutscheincodes oder API-Schlüssel mit einem einzigen Klick zu teilen.

useOnlineStatus ermöglicht es der App, auf Unterbrechungen oder Wiederherstellung der Netzwerkverbindung zu reagieren.

import { useState, useEffect } from "react";
function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);  useEffect(() => {
    window.addEventListener("online", () => setOnline(true));
    window.addEventListener("offline", () => setOnline(false));
  }, []);  return online;
}

Es handelt sich um eine kleine Ergänzung, doch sie verbessert deutlich das Nutzererlebnis bei unzuverlässiger Verbindbarkeit.

useDarkMode kümmert sich um den Schalter für den Dunkelmodus, den die meisten Nutzer heutzutage von modernen Apps erwarten.

import { useEffect } from "react";
function useDarkMode(enabled) {
  useEffect(() => {
    document.body.classList.toggle("dark", enabled);
  }, [enabled]);
}

Es wird üblicherweise mit useLocalStorage kombiniert, damit der gewählte Modus über verschiedene Sitzungen hinweg erhalten bleibt.

Schließlich macht useTimeout die Arbeit mit Verzögerungen viel übersichtlicher als das Versenden von rohen setTimeout-Aufrufen in den Komponenten.

import { useEffect } from "react";
function useTimeout(callback, delay) {
  useEffect(() => {
    const timer = setTimeout(callback, delay);    return () => clearTimeout(timer);
  }, [callback, delay]);
}

Es eignet sich für Benachrichtigungen, Startbildschirme sowie alle Aktionen, die nach einer Verzögerung ausgeführt werden müssen.

In vielen React-Projekten gilt stets dieselbe Lektion: Man muss nicht jedes Mal alles von Grund auf neu erstellen. Das Halten einer kleinen Sammlung wiederverwendbarer Hooks beschleunigt die Entwicklung, macht Komponenten leichter lesbar und beseitigt wiederholten Code. Keiner dieser zwölf Hooks ist besonders komplex, doch zusammen sparen sie im Laufe eines Projekts erheblich Zeit. Wenn Ihre eigenen Projekte wachsen, werden Sie wahrscheinlich eine ähnliche persönliche Sammlung anlegen – und entscheidend ist nicht die Gesamtzahl der Hooks, sondern ob sie die Probleme lösen, auf die man immer wieder stößt. Ein kleines Hilfsmittel, das Sie heute für ein Projekt schreiben, wird oft zu etwas, auf das man in allen folgenden Projekten zurückgreift.

Das Verständnis einer persönlichen Sammlung von Hooks ist im Alltag nützlich, doch genauso wertvoll ist es, die zugrunde liegenden Mechanismen zu verstehen – was jeder Kernhook löst, wann er verwendet werden sollte und wie sich sein Verhalten bei verschiedenen Darstellungen zeigt. Auf dieser tiefgreifenderen Grundlage hört die Nutzung von Hooks auf, reines Kopieren und Einfügen zu sein, und wird zu einem echten Verständnis.

Vor den Hooks konnten Funktionskomponenten keinen Zustand speichern oder Nebeneffekte ausführen; daher befand sich diese Logik in Klassenkomponenten mit einem state-Objekt und Lebenszyklusmethoden:

class Counter extends React.Component {
  state = {
    count: 0
  };

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  };

  render() {
    return (
      <button onClick={this.increment}>
        {this.state.count}
      </button>
    );
  }
}

Mit den Hooks wird dieselbe Komponente zu einer einfachen Funktion, die direkt useState aufruft:

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count}
    </button>
  );
}

Die funktionale Version ist in der Regel leichter zu lesen und wiederverwendbar.

Zwei Regeln gelten für Hooks. Erstens sollten sie immer auf der obersten Ebene aufgerufen werden – niemals innerhalb von Bedingungen, Schleifen oder verschachtelten Funktionen. Vermeiden Sie das:

if (isLoggedIn) {
  useEffect(() => {
    // ❌
  }, []);
}


for (...) {
  useState(0); // ❌
}

Halten Sie stattdessen den Hook-Aufruf unbedingt und platzieren Sie die bedingte Logik danach:

function Component() {
  const [count, setCount] = useState(0);

  if (count > 10) {
    // Normal conditional logic is fine
  }

  return ...;
}

Diese Regel existiert, weil React den Zustand nach der Aufrufreihenfolge und nicht nach dem Namen verfolgt. Angesichts zweier useState-Aufrufe:

function Component() {
  const [count] = useState(0);
  const [name] = useState("Salim");

  return ...;
}

Reaktiert sie positionell aneinander:

Hook #1 → count
Hook #2 → name

Falls der erste Aufruf bedingt wird:

if (condition) {
  useState(0);
}

useState("Salim");

kann sich die Reihenfolge zwischen den Renderungen ändern:

Render 1:
Hook #1 → count
Hook #2 → name

Render 2:
Hook #1 → name

Sobald das geschieht, kann React den gespeicherten Zustand nicht mehr mit dem richtigen Aufruf abgleichen, weshalb die Reihenfolge bei jedem Render konstant bleiben muss.

useState ermöglicht es einem Komponenten, einen Wert über mehrere Renderungen hinweg zu speichern:

const [count, setCount] = useState(0);

Es gibt ein Paar zurück:

count     → current state
setCount  → state update function

Ein einfacher Zähler zeigt dieses Muster:

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

Das Klicken auf die Schaltfläche löst diese Abfolge aus:

Click
 ↓
setCount()
 ↓
React schedules update
 ↓
Component renders again
 ↓
New state is calculated
 ↓
DOM is updated if necessary

Der Zustand ist keine gewöhnliche Variable wie let count = 0, da diese bei erneutem Ausführen der Funktion nicht erhalten bleibt. React speichert ihn außerhalb des Komponenten und liefert den richtigen Wert bei jeder Renderung:

Render 1
count = 0

Render 2
count = 1

Render 3
count = 2

Die Komponente wird erneut ausgeführt, doch React bewahrt den Zustand zwischen den Ausführungen auf.

Dies ist wichtig, wenn der Zustand mehrmals innerhalb eines Handlers aktualisiert wird:

setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

Falls die Renderung mit einem count-Wert von 0 begann, verwenden alle drei Zeilen denselben Zustandswert, wodurch:

setCount(1)
setCount(1)
setCount(1)

statt drei tatsächlichen Inkrementen entstehen. Die Lösung besteht in einer funktionalen Aktualisierung:

setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);

Nun wendet React jede Aktualisierung nacheinander an, da jeder Aktualisierungscode den neuesten Wert erhält – nützlich, wenn neuer Zustand von altem Zustand abhängt.

useEffect synchronisiert eine Komponente mit etwas außerhalb von React:

useEffect(() => {
  // synchronization logic
}, [dependencies]);

Typische Fälle sind API-Aufrufe, Timer, Event-Listener, WebSockets, andere Browser-APIs, Abonnements sowie Bibliotheken Dritter. Beispiel:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  return <h1>{user?.name}</h1>;
}

Dieser Effekt wird mit userId synchronisiert; wenn dieser sich ändert, muss der Effekt erneut ausgeführt werden.

Dieses Verhalten stammt aus dem Abhängigkeitsarray. Gegeben:

useEffect(() => {
  console.log(count);
}, [count]);

React vergleicht die Abhängigkeiten konzeptionell:

Previous dependencies
        ↓
New dependencies
        ↓
Compare
        ↓
Changed?

Falls sie unterschiedlich sind, wird der Effekt erneut ausgeführt:

Previous: [1]
New:      [2]

→ Effect needs to run

Falls sie übereinstimmen, wird er übersprungen:

Previous: [2]
New:      [2]

→ Effect can be skipped

Der Vergleich erfolgt nach Object.is, nicht nach tiefer Gleichheit.

Effekte können eine Aufräumfunktion zurückgeben, die vor dem nächsten Effekt sowie beim Entfernen ausgeführt wird:

useEffect(() => {
  const timer = setInterval(() => {
    console.log("tick");
  }, 1000);

  return () => {
    clearInterval(timer);
  };
}, []);

Die Aufräumfunktion ist wichtig für Dinge wie:

Timers
Event listeners
Subscriptions
WebSocket connections
Abortable requests

Wenn sich Abhängigkeiten ändern, räumt React den alten Effekt auf, bevor der neue ausgeführt wird; er räumt ihn auch auf, wenn das Komponente entmontiert wird.

Eine Sucheingabe zeigt dies in der Praxis – jede Tastenbetätigung kann eine Anfrage auslösen:

react
react hooks
react performance

Da eine veraltete Antwort eine neuere überschreiben könnte, muss die vorherige Anfrage storniert werden:

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(setResults)
    .catch(error => {
      if (error.name !== "AbortError") {
        console.error(error);
      }
    });

  return () => {
    controller.abort();
  };
}, [query]);

Der Lebenszyklus sieht so aus:

query changes
     ↓
cleanup previous effect
     ↓
abort previous request
     ↓
start new request

Echte Sucheingaben kombinieren dies in der Regel mit Debouncing.

Ein häufiger Fehler ist die Verwendung von useEffect für Werte, die direkt abgeleitet werden können:

const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Da fullName direkt aus vorhandenen Werten stammt, entfällt sowohl der Effekt als auch der Zustand ganz:

const fullName = `${firstName} ${lastName}`;

Die Effekt-Version führt unnötige Synchronisierung sowie eine zusätzliche Renderung mit sich. Faustregel: Wenn man einen Wert während des Renders berechnen kann, braucht man keinen Effekt dafür.

useRef stellt ein stabiles Objekt bereit, dessen .current-Eigenschaft über mehrere Renderungen hinweg erhalten bleibt, ohne dass eine erneute Renderung ausgelöst wird, wenn sich dieser Wert ändert:

const ref = useRef(initialValue);

Eine häufige Anwendung ist die Verweisung auf einen DOM-Node, beispielsweise um eine Eingabe zu fokussieren:

function Input() {
  const inputRef = useRef(null);

  function focusInput() {
    inputRef.current?.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>
        Focus
      </button>
    </>
  );
}

useRef eignet sich auch zum Speichern von veränderbaren Werten, die überhaupt nicht im renderierten Ausgabeinhalt vorkommen sollten, wie beispielsweise ein Timer-Identifikator:

const timerRef = useRef(null);

Dann weist man ihm direkt Werte zu, genauso wie man es bei einer beliebigen Objekt-Eigenschaft tun würde:

timerRef.current = setInterval(...);

Die Veränderung

ref.current

triggert an sich keine erneute Renderung. Vergleichen Sie die beiden Denkmodelle:

useState
→ update → render

useRef
→ mutate .current → no render

Deshalb sind Refs besonders geeignet für Werte, die zwischen Renderungen bestehen bleiben sollen, aber keinen Einfluss auf das Auf dem Bildschirm Dargestellte haben sollten.

useMemo verfolgt einen anderen Ansatz: Es speichert das Ergebnis einer Berechnung statt eine DOM-Referenz.

const result = useMemo(() => {
  return expensiveCalculation(data);
}, [data]);

Stellen Sie es sich wie folgt vor: useMemo = Berechnung speichern. Ein typischer Anwendungsfall ist das Filtern einer Liste nur dann, wenn sich die zugrunde liegenden Daten tatsächlich ändern:

function ProductList({ products, search }) {
  const filteredProducts = useMemo(() => {
    return products.filter(product =>
      product.name
        .toLowerCase()
        .includes(search.toLowerCase())
    );
  }, [products, search]);

  return <ProductGrid products={filteredProducts} />;
}

Falls sich ein nicht verwandter Zustandswert ändert, kann React den zuvor berechneten Wert einfach weitergeben, solange die Abhängigkeiten gleich bleiben. Konzeptionell speichert React eine kurze Aufzeichnung bei jedem Aufruf des Hooks:

Hook
 ├── memoized value
 └── dependencies

Beim ersten Render:

data = A

calculate(A)
 ↓
result = X

Bei einem späteren Render, wenn data weiterhin A ist, besteht die Abhängigkeitsprüfung und React wiederverwendet X ohne erneute Berechnung. Erst wenn data zu etwas wie B wird, führt React die Berechnung erneut aus.

Doch useMemo kann leicht übermäßig verwendet werden. Etwas wie Folgendes zu umhüllen:

const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);

lohnt sich selten – die Berechnung ist trivial, und die Memoisierung selbst kostet etwas; sie führt zu zusätzlichem Overhead sowie Code, den man berücksichtigen muss. Wenden Sie useMemo an, wenn die Berechnung tatsächlich aufwendig ist, wenn Sie eine stabile Referenz auf ein Objekt oder einen Array benötigen, oder wenn Profilanalysen (oder logisches Nachdenken) zeigen, dass es tatsächlich hilft.

useCallback ist das entsprechende Werkzeug für Funktionenreferenzen anstelle von Werten.

const handleClick = useCallback(() => {
  doSomething();
}, []);

Das ist wichtig, weil gewöhnliche Funktionen bei jeder Neuansicht neu erstellt werden:

function Parent() {
  const handleClick = () => {
    console.log("clicked");
  };

  return <Child onClick={handleClick} />;
}

Bei der ersten Neuansicht zeigt handleClick auf eine Funktionseinheit; bei der zweiten Neuansicht zeigt es auf eine andere, sodass die beiden zwar dasselbe tun, aber nicht gleich sind.

Diese Ungleichheit wird zum Problem, wenn ein Kind-Komponente in React.memo eingehüllt ist:

const Child = React.memo(function Child({ onClick }) {
  console.log("Child rendered");

  return (
    <button onClick={onClick}>
      Click
    </button>
  );
});

und der Elternteil sieht so aus:

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        {count}
      </button>

      <Child onClick={handleClick} />
    </>
  );
}

Wenn sich count ändert:

Parent renders
      ↓
handleClick reference remains stable
      ↓
React.memo checks Child props
      ↓
onClick unchanged
      ↓
Child can skip rendering

Ohne useCallback würde die neu erstellte Funktionsreferenz vom gememorisierten Kind als „neu“ angesehen werden, wodurch es erneut gerendert wird, obwohl sich für es nichts Wesentliches geändert hat.

Trotzdem sollte useCallback nicht standardmäßig auf jede Funktion angewendet werden. Bevor man einen Handler einhüllt, sollte man prüfen, ob tatsächlich etwas von einer stabilen Referenz profitiert – zum Beispiel:

React.memo child
Dependency array
Expensive downstream computation

Falls keines davon zutrifft, ist es in der Regel in Ordnung, wenn die Funktion normal neu erstellt wird.

Der Unterschied zwischen den beiden Hooks liegt darin, was sie memorisieren:

useMemo
→ memoizes a VALUE

useCallback
→ memoizes a FUNCTION

Conceptually:

useMemo(() => calculateValue(), deps);

useCallback(() => doSomething(), deps);

Indem man von leistungsorientierten Hooks zu einem strukturellen Ansatz wechselt, befasst sich die Context API mit einem anderen Problem: dem Übertragen von Daten durch viele Ebenen verschachtelter Komponenten. Ohne sie müsste ein Wert wie der aktuelle Benutzer durch jede Zwischenebene geleitet werden:

App
 ↓
Navbar
 ↓
UserMenu
 ↓
Profile
 ↓
Avatar

was zu etwas wie folgendem führt:

<App user={user} />
<Navbar user={user} />
<UserMenu user={user} />
<Profile user={user} />
<Avatar user={user} />

Dieses Muster des Weiterleitens von Props an Komponenten, die sie sonst nicht benötigen, wird als Prop Drilling bezeichnet.

Die Einrichtung des Contexts beginnt mit einem Erstellungsaufruf:

const UserContext = createContext(null);

Dann wird mit einem Provider ein Wert bereitgestellt:

function App() {
  const user = {
    name: "Salim",
    role: "Developer"
  };

  return (
    <UserContext.Provider value={user}>
      <Dashboard />
    </UserContext.Provider>
  );
}

und dieser wird dort gelesen, wo er benötigt wird:

function Profile() {
  const user = useContext(UserContext);

  return <h1>Hello {user.name}</h1>;
}

Profile benötigt den user-Wert nicht mehr, der durch jede Zwischenkomponente weitergeleitet werden muss.

Ein häufiges Szenario in der Praxis ist das Theming:

const ThemeContext = createContext(null);

function App() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Dashboard />
    </ThemeContext.Provider>
  );
}

Jeder Komponente kann dann das aktuelle Theme direkt lesen:

function Button() {
  const { theme } = useContext(ThemeContext);

  return (
    <button className={theme}>
      Submit
    </button>
  );
}

was einen etwa baumförmigen Aufbau ergibt:

App
 │
 └── ThemeProvider
       │
       └── Dashboard
             │
             └── Button

Button holt den Theme-Wert ohne jegliches Prop-Drilling ab.

Es ist wichtig zu betonen, dass Context nicht automatisch eine vollständige State-Management-Lösung darstellt. Er beantwortet nur eine spezifische Frage: Wie macht man einen Wert für Komponenten tief im Baum zugänglich? Er bietet nicht die zusätzlichen Mechanismen – Selektoren, Middleware, strukturierte Aktualisierungen –, die eine speziell dafür konzipierte Bibliothek bereitstellt. Für anspruchsvollere State-Anforderungen könnte man sich an Folgendes wenden:

Redux
Zustand
Jotai
Reducer + Context

Context sollte am besten als Wertverteilungsmechanismus und nicht als State-Manager betrachtet werden.

Außerdem hat er Auswirkungen auf die Darstellung. Unter der Voraussetzung:

<ThemeContext.Provider value={theme}>

Immer wenn sich der Wert dieses Kontexts ändert, können die Komponenten, die ihn verwenden, neu gerendert werden. Der Kontext verhindert Neu-Renderungen nicht auf magische Weise – das Hineinbringen eines großen, häufig ändernden Objekts in einen weit verbreiteten Kontext kann tatsächlich unnötige Arbeit verursachen. Bessere Kandidaten für einen Kontext sind Werte, die nur selten ändern, wie zum Beispiel:

Theme
Locale
Authentication information
Feature flags
Application configuration

Custom Hooks ermöglichen es Ihnen, wiederverwendbare Logik aus den eingebauten Hooks zu bündeln. Ein minimales Beispiel:

function useCounter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount(c => c + 1);
  }

  return {
    count,
    increment
  };
}

So wird es verwendet:

function Counter() {
  const { count, increment } = useCounter();

  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}

Der eigentliche Wert hier liegt im Wiederverwenden von Logik, nicht im Wiederverwenden von Markup.

Dies ist hervorzuheben: Custom Hooks teilen Logik, nicht Zustand. Wenn zwei separate Komponenten jeweils denselben Custom Hook aufrufen:

const counterA = useCounter();
const counterB = useCounter();

teilen sie keinen einzigen count. Jeder Aufruf erhält seinen eigenen unabhängigen Zustand. Konzeptionell gesehen:

Component A
 └── useCounter
      └── useState → State A

Component B
 └── useCounter
      └── useState → State B

Ein benutzerdefinierter Hook verpackt Verhalten, nicht einen gemeinsamen Datenspeicher.

Zum konkreten Beispiel: Stellen Sie sich vor, eine Anwendung muss anzeigen, ob der Benutzer derzeit eine Netzwerkverbindung hat. Anstatt die Logik zum Abhören von Browser-Ereignissen in jedem Komponenten, der sie benötigt, zu duplizieren, können Sie diese in einen benutzerdefinierten Hook einpacken:

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    navigator.onLine
  );

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }

    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return isOnline;
}

Jede Komponente, die den Verbindungsstatus benötigt, kann ihn nun direkt nutzen:

function Navbar() {
  const isOnline = useOnlineStatus();

  return (
    <span>
      {isOnline ? "🟢 Online" : "🔴 Offline"}
    </span>
  );
}

Eine andere Komponente könnte denselben Hook verwenden, um ihre eigene Darstellung vollständig zu ändern:

function Checkout() {
  const isOnline = useOnlineStatus();

  if (!isOnline) {
    return <p>You are offline.</p>;
  }

  return <PaymentForm />;
}

Ein ähnliches Muster gilt für das Abfedern schnell wechselnder Eingaben:

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

Es wird in einer Suchfunktion wie dieser verwendet:

function Search() {
  const [query, setQuery] = useState("");

  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (!debouncedQuery) return;

    // Search API
  }, [debouncedQuery]);

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
    />
  );
}

was diese Abfolge von Ereignissen erzeugt:

User types
    ↓
query changes
    ↓
500ms wait
    ↓
debouncedQuery changes
    ↓
API request

Diese Bausteine sind dazu gedacht, miteinander kombiniert zu werden, nicht isoliert verwendet zu werden:

                 React Component
                       │
       ┌───────────────┼────────────────┐
       │               │                │
   useState        useEffect        useContext
       │               │                │
    UI state       External systems   Shared data
       │
       └───────────────┐
                       │
                 Custom Hook
                       │
             ┌─────────┼─────────┐
             ▼         ▼         ▼
         useState   useEffect   useMemo

Ein benutzerdefinierter useProducts()-Hook könnte beispielsweise intern auf Folgendes angewiesen sein:

useState
+
useEffect
+
useMemo

beim Abrufen des Authentifizierungszustands aus useContext. Die erneute Darstellung folgt einem vorhersehbaren Ablauf:

State / Props / Context change
          ↓
      React update
          ↓
       Render
          ↓
   Reconciliation
          ↓
       Commit
          ↓
      DOM update
          ↓
      Browser paint

jeder Hook übernimmt dabei eine eigene Rolle, die hier zusammengefasst wird:

useState
→ provides state + schedules updates

useMemo
→ calculates/reuses values during render

useCallback
→ calculates/reuses function references during render

useContext
→ reads context during render

useEffect
→ synchronizes with external systems after commit

Verwandte Literatur

  • React Components 101: Erstellung wiederverwendbarer, wartbarer UI-Elemente — Erfahren Sie, warum die Aufteilung der Benutzeroberfläche in kleine React-Komponenten die Wiederverwendbarkeit, Lesbarkeit und die Teamzusammenarbeit verbessert, und erstellen Sie anschließend Ihre erste funktionale Komponente.
  • Warum catch () in JavaScript einen SyntaxError auslöst — Erfahren Sie, warum eine leere Parameterliste für catch die JavaScript-Parsing-Funktion vollständig stört, und sehen Sie sich die beiden grammatikalisch korrekten Möglichkeiten an, einen parameterlosen catch-Block zu schreiben.
  • Stoppen Sie die Synchronisierung des Zustands mit useEffect: Ein sichereres React-Muster — Erfahren Sie, warum die Verwendung von useEffect zur Synchronisierung des abgeleiteten Zustands zu Wettbewerbsbedingungen und zusätzlichen Render-Vorgängen führt, sowie wie man dies durch Ableitung zum Zeitpunkt des Renders und die key-Attribut ersetzen kann.