React context ist der offizielle Weg, Daten durch einen Komponentenbaum zu teilen, ohne sie auf jeder Ebene von Hand als props weiterzureichen. Wenn du jemals jeder Komponente in einer Kette eine überflüssige Prop hinzugefügt hast, nur um etwa das Theme, den angemeldeten Nutzer oder die gewählte Sprache an eine fünf Ebenen tiefe Komponente zu liefern, bist du auf das Problem namens "Prop Drilling" gestoßen. In diesem Artikel zeige ich, wie man die Context API korrekt einrichtet, in einen Custom Hook verpackt, die häufigsten Performance-Fallen vermeidet und erkennt, wann man context gerade nicht einsetzen sollte — alles mit konkretem Code.
Was Prop Drilling genau ist
Angenommen, die Session-Infos leben ganz oben und nur eine tief verschachtelte Avatar-Komponente nutzt sie. Die dazwischenliegenden Komponenten Layout, Sidebar und UserMenu lesen den Wert nie, müssen aber eine Prop akzeptieren, nur um sie weiterzureichen:
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
// Layout -> Sidebar -> UserMenu -> Avatar
// jede reicht "user" nur an die darunterliegende weiter
Das Problem: Die Signatur der Zwischenkomponenten wird mit Daten verschmutzt, von denen sie nicht abhängen, Refactoring wird schwieriger, und das Umbenennen einer einzigen Prop zieht sich durch die ganze Kette. Context schließt diese Kette kurz: Du stellst den Wert einmal oben bereit und liest ihn direkt unten.
Das kleinste funktionierende Beispiel
Context besteht aus drei Teilen: die Context mit createContext erstellen, einen Wert über einen Provider verteilen und diesen Wert mit useContext lesen.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggle = () => setTheme(t => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
Umschließe anschließend die Wurzel deiner App mit dem Provider und rufe den Hook aus jeder Nachkommen-Komponente auf:
function ThemeButton() {
const { theme, toggle } = useTheme();
return <button onClick={toggle}>Theme: {theme}</button>;
}
function App() {
return (
<ThemeProvider>
<ThemeButton />
</ThemeProvider>
);
}
Egal wie viele Schichten nun zwischen ThemeButton und App liegen, keine muss eine theme-Prop tragen.
Sicherer Zugriff mit einem Custom Hook
Oben habe ich einen kleinen Custom Hook namens useTheme geschrieben; bevorzuge stets dieses Muster. Es hat zwei Vorteile: Es verbirgt die Interna der Context vor dem Aufrufer und kann einen aussagekräftigen Fehler werfen, wenn es ohne Provider verwendet wird.
export function useTheme() {
const ctx = useContext(ThemeContext);
if (ctx === undefined) {
throw new Error("useTheme muss innerhalb eines <ThemeProvider> verwendet werden");
}
return ctx;
}
Wenn du den Anfangswert mit createContext(undefined) setzt, bekommst du beim Vergessen des Providers einen klaren Fehler, statt stillschweigend einen falschen Standardwert zurückzubekommen. Im Team beschleunigt das die Fehlersuche erheblich.
Die größte Performance-Falle: unnötige Re-Renders
Das am häufigsten missverstandene an context ist dies: Wenn sich die value des Providers ändert, rendern alle Komponenten, die diese Context konsumieren, neu. Die Probleme beginnen, wenn du bei jedem Render eine neue Objektreferenz erzeugst:
// SCHLECHT: ein neues { } Objekt pro Render -> alle Konsumenten rendern neu
<ThemeContext.Provider value={{ theme, toggle }}>
Selbst wenn die Provider-Komponente aus einem unzusammenhängenden Grund neu rendert, erzeugt diese Zeile ein neues Objekt, und da React Referenzen mit Object.is vergleicht, gilt es als "geändert." Die Lösung ist, den Wert mit useMemo zu stabilisieren:
const value = useMemo(() => ({ theme, toggle }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
Eine zweite Technik ist, die Context zu teilen: Wenn du häufig wechselnde Daten (z. B. eine Live-Texteingabe) und selten wechselnde Funktionen (z. B. dispatch) in zwei getrennte Contexts legst, betrifft der schnell wechselnde Wert nur die Komponenten, die ihn lesen. Faustregel: Je mehr unzusammenhängende Dinge eine einzelne Context trägt, desto höher das Risiko verschwenderischer Re-Renders.
Kombination mit useReducer
Wenn die State-Logik wächst, ist useReducer statt useState in Verbindung mit context ein sauberes Muster. Es verhält sich wie ein kleiner globaler "Store", braucht aber keine externe Bibliothek:
const CartContext = createContext(null);
function cartReducer(state, action) {
switch (action.type) {
case "add": return [...state, action.item];
case "remove": return state.filter(i => i.id !== action.id);
default: return state;
}
}
export function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
const value = useMemo(() => ({ items, dispatch }), [items]);
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
React hält die dispatch-Funktion stabil, du kannst sie also gefahrlos in den Abhängigkeits-Arrays von memo verwenden.
Wann man context nicht verwenden sollte
Context ist nicht die Antwort auf jedes Problem. In den folgenden Situationen schau dich anderweitig um:
- Hochfrequente Updates mit breiter Wirkung (z. B. Tausende Abonnenten bei jedem Tastenanschlag): context allein ist kein auf Performance optimierter State-Manager; Werkzeuge wie Zustand, Jotai oder Redux Toolkit sind in diesem Szenario effizienter.
- Serverdaten (von einer API, die Caching und Wiederholungen brauchen): eine Datenschicht wie TanStack Query passt hier weitaus besser als context.
- Nur ein bis zwei Ebenen tief: Wenn das Prop Drilling zwei Schichten umfasst, ist das Durchreichen einer Prop oft lesbarer als das Aufsetzen einer Context.
Der Sweet Spot von context sind Daten, die sich selten ändern, aber an vielen Stellen gelesen werden: Theme, Authentifizierungsstatus, Sprache/Lokalisierung, Feature-Flags.
Häufige Fragen
Ersetzt context Redux?
Teilweise. Context ist ein Mechanismus zum "Datentransport", keine vollwertige State-Management-Bibliothek. Für einfachen globalen State reicht useReducer + context; sobald du jedoch Middleware, Devtools oder selektorbasierte Render-Optimierung brauchst, verdient sich Redux Toolkit oder Zustand seinen Platz.
Ist das Verschachteln mehrerer Contexts ein Problem?
Nein, es ist ein gängiges und gesundes Muster. Getrennte Provider für Theme, Nutzer und Warenkorb zu verwenden ist meist sowohl für die Performance als auch für die Lesbarkeit besser als eine einzige riesige Context. Du hältst die Wurzel sauber, indem du die verschachtelten Provider in einer einzigen AppProviders-Komponente bündelst.
Ist useMemo wirklich nötig?
Wenn der Provider-Wert ein Objekt oder eine Funktion enthält und der Provider neu rendern kann, ja. Ist der Wert ein primitiver Typ (string, number, boolean), vergleicht React den Wert statt einer Referenz, und useMemo ist nicht nötig.
Möchtest du den State-Fluss in deiner React-Architektur vereinfachen? Ich helfe dir, ein performantes, Prop-Drilling-freies context-Setup aufzubauen — nimm Kontakt auf.