Was ist die Context API?
Die Context API ist die integrierte Lösung von React, um Daten durch einen Komponentenbaum zu reichen, ohne Props durch jede einzelne Ebene schleifen zu müssen. Sie löst ein spezifisches, häufig auftretendes Problem: ein Wert wird von vielen Komponenten benötigt, befindet sich aber weit über ihnen im Baum.
Das klassische Beispiel ist das Theming. Eine Button tief im Baum benötigt das aktuelle Theme, aber jede Komponente zwischen der obersten Ebene und dem Button müsste eine theme Prop akzeptieren und weiterreichen. Das nennt man Prop Drilling, und Context eliminiert genau das. Du stellst den Wert einmal bereit, und jeder Nachfahre kann ihn direkt auslesen.
Kontext erstellen und bereitstellen
Ein Kontext besteht aus drei Teilen: einem Objekt, einem Provider und einem Consumer-Hook.
// AuthContext.jsx
import { createContext, useContext, useMemo, useState } from "react";
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(
() => ({
user,
login: (nextUser) => setUser(nextUser),
logout: () => setUser(null),
}),
[user],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
createContext definiert den Kontext und einen Standardwert. Der Provider stellt den aktuellen Wert für alle darunterliegenden Komponenten bereit. Der benutzerdefinierte Hook liest den Wert aus und gibt eine klare Fehlermeldung aus, falls die Komponente außerhalb des Providers verwendet wird.
Context konsumieren
Jeder Descendant kann den Wert lesen, unabhängig davon, wie tief er im Baum sitzt.
// Header.jsx
import { useAuth } from "./AuthContext";
export function Header() {
const { user, logout } = useAuth();
return (
<header>
<span>{user ? `Hi, ${user.name}` : "Guest"}</span>
{user && <button onClick={logout}>Sign out</button>}
</header>
);
}
Die Header hat niemals ein Prop von App erhalten. Sie hat stattdessen direkt im Baum nach dem benötigten Wert gesucht, wodurch Zwischenkomponenten nichts von Daten erfahren, die sie nicht selbst verwenden.
Context ist kein State
Diese Unterscheidung führt oft zu Verwirrung. Context hält keinen State und verwaltet keine Updates. Er transportiert lediglich einen Wert. Der State selbst lebt in useState oder useReducer, und der Provider reicht ihn nach unten weiter.
Aus diesem Grund ist es ein gängiges Pattern, Context mit einem Reducer zu kombinieren:
// CartContext.jsx
import { createContext, useContext, useReducer } from "react";
const CartContext = createContext(null);
function reducer(state, action) {
switch (action.type) {
case "add":
return [...state, action.item];
case "remove":
return state.filter((item) => item.id !== action.id);
default:
return state;
}
}
export function CartProvider({ children }) {
const [items, dispatch] = useReducer(reducer, []);
return (
<CartContext.Provider value={{ items, dispatch }}>
{children}
</CartContext.Provider>
);
}
export const useCart = () => useContext(CartContext);
Der Reducer sorgt dafür, dass Updates vorhersehbar bleiben, und Context stellt sie überall zur Verfügung, ohne dass Prop Drilling nötig ist.
Performance: die eigentliche Einschränkung
Wenn sich der Wert eines Providers ändert, rendert jede Komponente, die diesen Context liest, neu. Es gibt keine Selektoren, weshalb man sich nicht nur auf einen Teil des Wertes abonnieren kann. Zwei Gewohnheiten helfen dabei, dies handhabbar zu halten.
Memoisiere den Wert. Ein Objekt-Literal erzeugt bei jedem Render-Vorgang eine neue Referenz. Das bedeutet, dass jeder Consumer neu rendert, selbst wenn sich inhaltlich nichts geändert hat.
// memoised
const value = useMemo(() => ({ user, logout }), [user]);
Teile Contexts auf. Platziere State und Actions in separaten Contexts oder teile sie nach Features auf. So rendert eine Komponente, die nur eine Action auslösen muss, nicht neu, wenn sich der State ändert.
// split.jsx
const CartStateContext = createContext(null);
const CartActionsContext = createContext(null);
Komponenten, die nur Actions aufrufen, abonnieren den Actions-Context. Da dessen Identität sich nie ändert, rendern diese Komponenten bei einem State-Update niemals neu. Dieser eine Trick löst die meisten Performance-Probleme im Zusammenhang mit Context.
Wann man Context verwenden sollte – und wann nicht
Context ist das richtige Werkzeug für:
- Themes und Farbmodi.
- Den authentifizierten Benutzer und die Session.
- Locale und Übersetzungen.
- Einen Router oder eine kleine, stabile Konfiguration.
Greifen Sie zu einem dedizierten Store, wenn Sie Folgendes haben:
- Häufige Updates, wie beim Tippen, Drag-and-Drop oder bei Animationen.
- Einen großen State-Tree, bei dem Komponenten Selektoren benötigen.
- Einen Bedarf an middleware, Persistenz oder Time-Travel-Devtools.
- Server-State, der Caching und Refetching erfordert.
Bibliotheken wie Zustand und Redux Toolkit bieten Selektoren an, sodass Komponenten nur Slices statt des gesamten Wertes abonnieren. Für Server-Daten übernimmt TanStack Query das gesamte Caching.
Best Practices
- Kapseln Sie
useContextin einem Custom Hook und prüfen Sie, ob ein Provider fehlt. - Memoisieren Sie Provider-Werte mit
useMemo. - Trennen Sie Kontexte nach Zuständigkeiten, insbesondere State von Actions.
- Halten Sie Kontext-Werte klein und fokussiert.
- Verwenden Sie
useReducer, wenn Updates komplex sind oder vom vorherigen State abhängen. - Verwenden Sie Kontext nicht für hochfrequente Updates; nutzen Sie stattdessen einen selector-basierten Store.
- Definieren Sie sinnvolle Standardwerte, damit Komponenten bei Bedarf auch außerhalb eines Providers rendern können.
Häufige Fehler
- Context als State-Manager zu behandeln und Selektoren zu erwarten.
- Ein Inline-Objekt als Provider-Value zu übergeben, was zu Re-Renders aller Komponenten führt.
- Nicht zusammengehörige Daten in einem einzigen, riesigen Context zu bündeln.
- Den Default-Wert zu vergessen, was zu einem Absturz bei
undefinedführt. - Context für Werte zu verwenden, die nur von ein oder zwei Komponenten benötigt werden.
- Zu erwarten, dass Context Re-Renders automatisch optimiert.
Wie geht es weiter?
Context ist das Fundament für das Teilen von State in React. Wenn man seine Grenzen kennt, weiß man, wann es Zeit für den nächsten Schritt ist. Vergleiche es mit Redux Toolkit für vorhersagbaren State in großen Anwendungen und Zustand für einen leichtgewichtigen Selector-Store. Für die Verwaltung von Server-Daten empfiehlt sich TanStack Query. In Vue wird das entsprechende Konzept im Pinia-Guide behandelt.