Grenzen von Context API in React
Grenzen von Context API
~14 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
Willkommen zurück! Ab hier setzen wir "React für Einsteiger" direkt fort – dieselbe produktkatalog-web-App, kein neues Projekt. Falls Sie diese Serie neu beginnen: Sie brauchen den Endstand der 26 Kapitel von "React für Einsteiger", um hier mitzumachen.
Startpunkt dieser Serie – Endstand von "React für Einsteiger"
produktkatalog-web/
├── index.html
├── package.json
├── vite.config.js
├── docker-compose.yml (MySQL-Container für Bewertungen)
├── server/ (eigener Node/Express-Server)
│ ├── package.json
│ ├── server.js
│ └── db.js
└── src/
├── main.jsx
├── App.jsx (Routing)
├── index.css
├── api/
│ ├── magentoApi.js (Magento-REST-API: Produkte)
│ └── reviewsApi.js (eigener Server: Bewertungen)
├── context/
│ └── AuthContext.jsx (einfacher Login-Zustand)
├── components/
│ ├── ProductCard.jsx
│ └── ProtectedRoute.jsx
├── hooks/
│ └── useDocumentTitle.js (Custom Hook)
└── pages/
├── ProductListPage.jsx
├── ProductDetailPage.jsx
├── ReviewsPage.jsx (verschachtelte Route)
├── LoginPage.jsx
└── AccountPage.jsx (geschützte Route)Was erwartet Sie in "React für Profis"?
Fünf Themenblöcke, aufeinander aufbauend, direkt am bestehenden Projekt: State-Management (Zustand, Redux Toolkit – die Grenzen von Context, die wir in diesem Kapitel aufdecken, praktisch lösen), Performance (Profiler, React.memo, Listen-Virtualisierung, Concurrent Features), Internals (Virtual DOM, React Fiber, Higher-Order Components, Render Props, Portals, Error Boundaries), TypeScript-Integration (das bestehende Projekt typisieren – die TypeScript-Sprache selbst, jedes Feature einzeln erklärt, ist Thema eines eigenen, separaten Tutorials) und Testing/Praxis (Vitest, Best Practices, Deployment, Interview-Vorbereitung).
Was Context API wirklich gut kann
Bevor wir über Grenzen sprechen: AuthContext aus "React für Einsteiger" war die RICHTIGE Lösung für sein Problem. Erinnern Sie sich an das "Prop Drilling"-Problem aus Kapitel 11 – ohne Context müssten user/login/logout durch JEDE Zwischenkomponente als Props durchgereicht werden, auch durch solche, die selbst gar nichts damit anfangen. Context löst GENAU das: einen Wert bereitstellen, den JEDE Nachfahrkomponente direkt abrufen kann, egal wie tief sie verschachtelt ist.
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
function login(username) {
setUser({ username });
}
function logout() {
setUser(null);
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth() must be used inside <AuthProvider>.');
}
return context;
}Das ist der Endstand aus "React für Einsteiger" – zur Erinnerung gezeigt, bevor wir ihn gleich erweitern, um ein Problem SICHTBAR zu machen.
Das eigentliche Problem: JEDER Verbraucher rendert bei JEDER Änderung neu
Hier wird es "Profi"-relevant: <AuthContext.Provider value={{{{ user, login, logout }}}}> übergibt bei JEDEM Render von AuthProvider ein NEUES Objekt-Literal als value – auch wenn user sich gar nicht geändert hat. React vergleicht value per Referenz (===), nicht per "hat sich der Inhalt geändert". Ändert sich die Referenz, rendern ALLE Komponenten neu, die irgendwo useContext(AuthContext) aufrufen – selbst solche, die nur user lesen und niemals login/logout benutzen. Context kennt keine "Teil-Abonnements" – man bekommt entweder den GESAMTEN Wert oder gar nichts.
Achtung: Das ist KEIN Bug, sondern bewusstes Design von React: Context ist für WERTE gedacht, die sich selten ändern (Theme, Sprache, eingeloggter Benutzer) und deren Verbraucher es akzeptieren können, bei jeder Änderung neu zu rendern. Für HÄUFIG wechselnde, GROSSE oder in viele unabhängige Teile zerlegbare Zustände wird Context schnell zum Performance-Problem.
Das Problem sichtbar machen: ein Render-Zähler
Reden ist gut, SEHEN ist besser. Wir bauen einen kleinen, wiederverwendbaren RenderCounter, der protokolliert, wie oft eine Komponente tatsächlich neu gerendert wird – ein Werkzeug, das wir im gesamten "Profis"-Tutorial wiederverwenden werden.
import { useRef } from 'react';
// Ein Debug-Werkzeug: zeigt, wie oft die Elternkomponente seit dem ersten
// Rendern neu gerendert wurde. useRef statt useState ist hier bewusst gewählt -
// ein useState-Update würde selbst ein zusätzliches Rendern auslösen und den
// Zähler verfälschen; useRef zählt "nebenbei" mit, ohne selbst Re-Renders zu erzeugen.
function RenderCounter({ label }) {
const renderCount = useRef(0);
renderCount.current += 1;
return (
<small style={{ opacity: 0.6 }}>
???? {label}: {renderCount.current}× gerendert
</small>
);
}
export default RenderCounter;Wichtig: renderCount.current += 1 steht DIREKT im Funktionskörper, nicht in einem useEffect. Das ist beabsichtigt – wir wollen JEDEN Render zählen, auch solche, die keine Seiteneffekte auslösen. useEffect würde nur nach dem COMMIT laufen, was für diesen Debug-Zweck unnötig verzögert wäre.
Den Beweis erbringen: unrelated state in AuthContext
Jetzt erweitern wir AuthContext testweise um einen Benachrichtigungs-Zähler, der NICHTS mit Login/Logout zu tun hat – nur um zu beweisen, was passiert:
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [notificationCount, setNotificationCount] = useState(0); // unrelated state - nur zu Demo-Zwecken
function login(username) {
setUser({ username });
}
function logout() {
setUser(null);
}
function addNotification() {
setNotificationCount((count) => count + 1);
}
return (
<AuthContext.Provider value={{ user, login, logout, notificationCount, addNotification }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth() must be used inside <AuthProvider>.');
}
return context;
}import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { useAuth } from './context/AuthContext';
import { useDocumentTitle } from './hooks/useDocumentTitle';
import RenderCounter from './components/RenderCounter';
import ProductListPage from './pages/ProductListPage';
import ProductDetailPage from './pages/ProductDetailPage';
import ProtectedRoute from './components/ProtectedRoute';
const ReviewsPage = lazy(() => import('./pages/ReviewsPage'));
const LoginPage = lazy(() => import('./pages/LoginPage'));
const AccountPage = lazy(() => import('./pages/AccountPage'));
function App() {
const { user, login, logout, notificationCount, addNotification } = useAuth();
useDocumentTitle('Product Catalog');
return (
<div className="app">
<header className="app-header">
<h1>Product Catalog</h1>
<RenderCounter label="App-Header" />
{user ? (
<p>Logged in as {user.username} <button onClick={logout}>Log out</button></p>
) : (
<button onClick={() => login('Jane Doe')}>Log in</button>
)}
<button onClick={addNotification}>
???? Notifications: {notificationCount} (klicken, um zu erhöhen)
</button>
</header>
<Suspense fallback={<p>Loading page...</p>}>
<Routes>
<Route path="/" element={<ProductListPage />} />
<Route path="/products/:sku" element={<ProductDetailPage />}>
<Route path="reviews" element={<ReviewsPage />} />
</Route>
<Route path="/login" element={<LoginPage />} />
<Route
path="/account"
element={
<ProtectedRoute>
<AccountPage />
</ProtectedRoute>
}
/>
</Routes>
</Suspense>
</div>
);
}
export default App;Starten Sie die App (npm run dev) und klicken Sie mehrmals auf den "???? Notifications"-Button. Der RenderCounter zählt bei JEDEM Klick hoch – obwohl App überhaupt nicht an notificationCount "interessiert" ist außer es anzuzeigen, und obwohl sich user dabei nicht ein einziges Mal ändert. Der Grund: App ruft useAuth() auf, und useAuth() liefert den KOMPLETTEN Context-Wert – React kann nicht wissen, dass App "eigentlich" nur an user/login/logout interessiert ist und notificationCount ignorieren könnte.
Warum das in echten Apps zum echten Problem wird
In unserer kleinen Demo-App ist ein zusätzlicher Render von App harmlos – die Komponente ist klein, das Neu-Rendern kostet Mikrosekunden. Das Problem skaliert aber SCHLECHT: Stellen Sie sich einen Context vor, der Warenkorb, Benachrichtigungen, Theme UND Login-Status bündelt (ein verlockender, aber gefährlicher "ein Context für alles"-Ansatz), und HUNDERTE Komponenten irgendwo im Baum, die davon lesen. Jede Warenkorb-Änderung würde dann JEDE dieser hundert Komponenten neu rendern – selbst eine, die nur die aktuelle Sprache anzeigt.
| Ansatz | Re-Render-Verhalten |
|---|---|
| Context API | Ein Provider, ein Wert-Objekt. JEDER Consumer bekommt bei JEDER Änderung des Objekts einen Re-Render – egal, welchen Teil des Objekts er tatsächlich nutzt. |
| Externe Stores (Zustand, Redux Toolkit) | Komponenten abonnieren gezielt EINZELNE Werte/Slices über einen Selector. Ändert sich ein anderer Teil des Stores, rendert die Komponente NICHT neu. |
Die Demo wieder aufräumen
Der Notifications-Button war reine Anschauung – bevor wir weitermachen, entfernen wir ihn wieder aus AuthContext.jsx und App.jsx und stellen den Stand von "React für Einsteiger" wieder her (Sie können das jetzt selbst tun: einfach notificationCount/addNotification aus beiden Dateien entfernen, RenderCounter bleibt als nützliches Debug-Werkzeug für die nächsten Kapitel erhalten). Das eigentliche, SAUBERE Problem lösen wir strukturell im nächsten Kapitel: Wir ersetzen AuthContext durch Zustand, eine kleine State-Management-Bibliothek, bei der Komponenten sich gezielt nur auf die Werte abonnieren, die sie wirklich brauchen.
Tipp: Merksatz für den Rest dieser Serie: Context API ist ein DEPENDENCY-INJECTION-Mechanismus ("gib mir Zugriff auf diesen Wert, egal wie tief ich verschachtelt bin"), kein PERFORMANCE-OPTIMIERTES State-Management-System. Beides zu verwechseln ist die häufigste Ursache für "meine React-App ruckelt"-Probleme in mittelgroßen bis großen Projekten.