Prop Drilling ist kein Grund, Redux oder Zustand zu installieren.
Testen Sie vier gängige Gründe für die Hinzufügung einer Staatsbibliothek zu funktionsfähigem React-Code: Context für das Prop-Drilling, useState, useSyncExternalStore sowie die Neuberechnungskosten von Context.
Fragen Sie ein Team, warum Redux, Zustand oder MobX in ihrer React-App verwendet werden – die Antwort ist meistens „Prop-Drilling“. Das ist zwar wirklich ärgerlich, aber kein guter Grund, eine Abhängigkeit hinzuzufügen. Auch die drei nachfolgenden Begründungen sind keine guten Gründe. Im Folgenden wird jede dieser vier Gründe anhand eines kleinen funktionierenden Beispiels überprüft, zusammen mit dem, was React bereits für diesen Fall bietet, sowie der Situation, in der eine State-Bibliothek sich tatsächlich lohnt.
Vier häufige Begründungen
Die Argumente kommen in der Regel in einer vorhersehbaren Reihenfolge:
- Der Übertrag von Werten durch Komponenten, die sie nicht verwenden, ist umständlich – daher benötigt die App einen Store.
- Klientischer State, der tatsächlich geändert wird, wie beispielsweise ein Flag für Light- oder Dark-Thema, benötigt sicherlich eine Bibliothek.
- Dass alle anderen Komponenten, die diesen State lesen, automatisch aktualisiert werden, ohne dass man die Props manuell verbindet, erfordert ebenfalls eine solche Bibliothek.
Jeder dieser Ansätze bricht zusammen, sobald man sich konkreten Code ansieht.
Prop drilling: Ein Problem, das React bereits vor Jahren gelöst hat
Prop drilling bedeutet, einen Wert über mehrere Ebenen weiterzugeben – ausschließlich damit ein tief verschachtelter Komponente ihn lesen kann, obwohl keine der Zwischenebenen ihn verwendet. Vier kleine Dateien zeigen die Struktur. App besitzt ein user-Objekt im State und gibt es an Dashboard weiter.
// App.jsx
import { useState } from 'react';
import Dashboard from './components/Dashboard';
function App() {
const [user, setUser] = useState({ name: 'Akshat', avatarUrl: '/me.png' });
return <Dashboard user={user} />;
}
export default App;
Dashboard tut mit user nichts anderes, als es an Sidebar weiterzuleiten.
// components/Dashboard.jsx
import Sidebar from './Sidebar';
function Dashboard({ user }) {
return <Sidebar user={user} />;
}
export default Dashboard;
Sidebar macht dasselbe und entpackt zwei Felder für Avatar.
// components/Sidebar.jsx
import Avatar from './Avatar';
function Sidebar({ user }) {
return <Avatar name={user.name} avatarUrl={user.avatarUrl} />;
}
export default Sidebar;
Nur Avatar rendernt tatsächlich die Daten.
// components/Avatar.jsx
function Avatar({ name, avatarUrl }) {
return <img src={avatarUrl} alt={name} />;
}
export default Avatar;
Niemand Dashboard.jsx noch Sidebar.jsx liest den Wert von user für ihre eigene Logik. Sie dienen lediglich als Übermittler. Wenn avatarUrl in photoUrl umbenannt wird, müssen beide Dateien angepasst werden – obwohl sich ihr Verhalten nicht ändert – einfach weil sie etwas enthalten, das ihnen nicht gehört.
Der Kontext liefert den Wert direkt
Die eingebaute Lösung von React ist der Context. Zunächst wird ein Kontextobjekt einmalig in seinem eigenen Modul erstellt.
// context/UserContext.js
import { createContext } from 'react';
const UserContext = createContext(null);
export default UserContext;
Dann umhüllt App den Unterbaum mit dem Provider des Kontexts und übermittelt user als dessen Wert, anstatt ihn als Eigenschaft weiterzugeben.
// App.jsx
import { useState } from 'react';
import UserContext from './context/UserContext';
import Dashboard from './components/Dashboard';
function App() {
const [user, setUser] = useState({ name: 'Akshat', avatarUrl: '/me.png' });
return (
<UserContext.Provider value={user}>
<Dashboard />
</UserContext.Provider>
);
}
export default App;
Dashboard und Sidebar reduzieren sich auf reine Layouts, ohne jegliche Erwähnung von user.
// components/Dashboard.jsx
import Sidebar from './Sidebar';
function Dashboard() {
return <Sidebar />;
}
export default Dashboard;
// components/Sidebar.jsx
import Avatar from './Avatar';
function Sidebar() {
return <Avatar />;
}
export default Sidebar;
Schließlich liest Avatar den Wert selbst.
// components/Avatar.jsx
import { useContext } from 'react';
import UserContext from '../context/UserContext';
function Avatar() {
const user = useContext(UserContext);
return <img src={user.avatarUrl} alt={user.name} />;
}
export default Avatar;
Jeder der drei Bestandteile hat eine bestimmte Aufgabe. createContext erstellt einen Kanal, der unabhängig von Props ist. Der Provider macht user sofort für den gesamten Unterbaum verfügbar. useContext(UserContext) liest den Wert vom nächstgelegenen passenden Provider über der Komponente, wobei alle dazwischenliegenden Ebenen übersprungen werden. Die Zwischenkomponenten erwähnen user nicht mehr, weil sie ihn nie benötigt haben. Nebenbei bemerkt ermöglicht React 19 es auch, das Context-Objekt direkt als Provider darzustellen, doch die hier gezeigte .Provider-Form funktioniert weiterhin.
Warum das Argument gegen „Prop-Drilling“ länger bestand als der ursprüngliche Grund
Die Geschichte erklärt, warum dieser Streit anhält. Redux erschien im Juni 2015, wurde von Dan Abramov und Andrew Clark entwickelt und basiert auf Facebooks Flux-Architektur: einem einzigen zentralen Store mit strengen Regeln dafür, wie sich der Zustand ändern darf. Reacts Context API wurde erst in Version 16.3 im März 2018, fast drei Jahre später, zu einer stabilen, offiziell unterstützten Funktion. In den Anfängen von Redux war ein Store tatsächlich die praktische Methode, um einen Wert überall im „Baum“ zugänglich zu machen, ohne ihn durch jedes Komponenten zu leiten.
Das galt nicht mehr, sobald Context stabil wurde, doch die Lehren haben dies nie mitbekommen. Tutorials setzten weiterhin auf Prop-Drilling als Grund für die Notwendigkeit, da dies sich leicht an einer Tafel darstellen lässt – und diese Gewohnheit blieb lange bestehen, nachdem der ursprüngliche Grund dafür verschwunden war.
Die Prop-Bohrung dreht sich jedoch um die Verteilung und nicht um Veränderungen. Die nächste Frage ist, was passiert, wenn der verteiltete Wert am Client anfängt zu ändern.
Den Zustand zu ändern, ist genau das, was useState tut
Nehmen wir einen Themen-Schalter: ein einzelner Flagge-Wert, der entweder light oder dark enthält, der durch einen Button umgeschaltet wird, der direkt neben dem Text angezeigt wird.
// components/SettingsPanel.jsx
import { useState } from 'react';
function SettingsPanel() {
const [theme, setTheme] = useState('light');
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
);
}
export default SettingsPanel;
Durch Klicken auf den Button wird setTheme aufgerufen, SettingsPanel wird mit dem neuen Wert neu gerendert und der Absatz aktualisiert sich. Es werden keine Bibliotheken verwendet, und es sind auch keine erforderlich. Einen Wert zu besitzen, ihn zu ändern und den zugehörigen Bestandteil neu zu rendern – genau dafür existiert useState, und jede React-Anwendung tut dies ständig.
Das Beispiel wird in der Regel mit einer Besonderheit präsentiert, die die eigentliche Arbeit erledigt. Die Schwierigkeit liegt nicht darin, dass sich der Wert ändert, sondern darin, dass der Wert an einer anderen Stelle gelesen wird. Stellen Sie sich vor, die Schaltfläche bleibt in SettingsPanel.jsx erhalten, während Header.jsx und Sidebar.jsx, zwei unverwandte Dateien, die weder importiert werden noch SettingsPanel importieren, ebenfalls das aktuelle Theme anzeigen müssen. Der mit useState erstellte Zustand gehört zu einer einzigen Komponenteninstanz. Nichts außerhalb dieser Komponente kann ihn lesen oder über Änderungen informiert werden, es sei denn, ein gemeinsamer Vorfahre gibt ihn weiter – was wiederum zu einem „Prop-Drilling“ führt.
Daher handhabt useState Änderungen hervorragend. Die offene Frage ist, wie zwei Komponenten ohne gemeinsamen Elternteil automatisch ohne Props aktualisiert werden können.
Zustand zwischen unverwandten Komponenten mit useSyncExternalStore teilen
Falls weder Header noch Sidebar den Wert besitzen können, muss er außerhalb des Komponentenbaums liegen: in einer modulweiten Variablen, die nur einmal initialisiert wird, wenn ihre Datei zum ersten Mal importiert wird, anstatt innerhalb einer Komponentenfunktion. React erkennt nie eine einfache Variable, der ein neuer Wert zugewiesen wird, daher kann es nichts eigenständig neu rendern, wenn sich dieser Wert ändert. Ein kleines Store-Modul schließt diese Lücke.
// store/themeStore.js
import { useSyncExternalStore } from 'react';
let state = { theme: 'light' };
const listeners = new Set();
export function setTheme(theme) {
state = { ...state, theme };
listeners.forEach((listener) => listener());
}
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function useTheme() {
return useSyncExternalStore(subscribe, () => state.theme);
}
Der Modul enthält ein state-Objekt sowie eine Set-Struktur mit Zuhörern. Die Funktion setTheme ersetzt das state-Objekt durch ein neues und ruft jeden Zuhörer auf. Eine private Registrierungsfunktion fügt einen Zuhörer zur Set-Struktur hinzu und gibt eine Aufräumfunktion zurück, mit der dieser wieder entfernt werden kann. useTheme verbindet all das mit React über useSyncExternalStore, einen Hook, der speziell für Zustände konzipiert ist, die außerhalb von Komponenten liegen und von mehreren davon gelesen werden.
Der Hook nimmt hier zwei Argumente entgegen. Das erste, die Registrierungsfunktion, weist React an, wie ein Callback angehängt werden soll, der jedes Mal ausgelöst wird, wenn sich der externe Wert ändert; sie muss außerdem eine entsprechende Aufräumfunktion zurückgeben. Das zweite Argument, getSnapshot, in diesem Fall die Pfeilfunktion () => state.theme, ist der Mechanismus, über den React den aktuellen Wert abruft, wann immer er benötigt wird.
Zwei Verbraucher importieren den Hook, und keiner von ihnen kennt den anderen.
// components/Header.jsx
import { useTheme } from '../store/themeStore';
function Header() {
const theme = useTheme();
return <header className={theme === 'dark' ? 'header-dark' : 'header-light'}>Site Header</header>;
}
export default Header;
// components/Sidebar.jsx
import { useTheme } from '../store/themeStore';
function Sidebar() {
const theme = useTheme();
return <aside className={theme === 'dark' ? 'sidebar-dark' : 'sidebar-light'}>Navigation</aside>;
}
export default Sidebar;
Ein dritter Komponente enthält die Schaltfläche und importiert sowohl den Hook als auch setTheme aus demselben Store.
// components/ThemeToggleButton.jsx
import { setTheme, useTheme } from '../store/themeStore';
function ThemeToggleButton() {
const theme = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}
export default ThemeToggleButton;
Was passiert, Schritt für Schritt
Jeder der folgenden Schritte ist sichtbar, wenn man den Code ausführt:
- Zur Initialisierung rufen
HeaderundSidebarjeweilsuseTheme()auf. React ruft für beidegetSnapshotauf, erhält den Wert ‚light‘ und rendernt sie damit. - React ruft außerdem die Registrierungsfunktion einmal pro Komponente auf, sodass zwei interne React-Callbacks dem gemeinsamen
listeners-Set hinzugefügt werden. Die Komponenten bleiben dabei unabhängige Einträge in diesem Set.
ThemeToggleButton ruft setTheme('dark') auf. Zunächst wird state auf ein völlig neues Objekt, { theme: 'dark' }, umgeleitet; dies ist reiner JavaScript-Code ohne etwas, das speziell für React ist.setTheme die Liste und ruft jeden Zuhörer auf. Da diese Zuhörer zu React gehören, veranlasst ihr Aufruf React dazu, jede registrierte Komponente zu überprüfen.getSnapshot für Header auf, erkennt anstelle von 'light' 'dark' und rendert die Komponente neu. Derselbe Check führt dazu, dass Sidebar ebenfalls neu gerendert wird.Dieser setTheme ist nicht der Setter, der von useState zurückgegeben wird. Es handelt sich um gewöhnlichen, manuell geschriebenen Code, der in einem Aufruf zwei Aufgaben erledigt: Er aktualisiert die Quelle der Wahrheit und informiert anschließend alle Zuhörer.
Es wurden keine Props übergeben. Die gesamte Verkabelung besteht aus Imports. Im Grunde genommen macht Zustand intern genau dasselbe: eine kleine, verpackte Version des gleichen Register-und-Benachrichtigungs-Musters.
Mit Bedacht zu nehmende Aspekte
getSnapshotmuss denselben Wert zurückgeben, wenn sich nichts geändert hat. Es ist sicher, einen Primitivwert wiestate.themezurückzugeben; bei jeder Aufrufung ein neues Objekt oder Array zu erstellen lässt React glauben, der Store ändere sich ständig.- Falls Sie auf dem Server rendern, akzeptiert
useSyncExternalStoreein drittes Argument,getServerSnapshot, für den initialen HTML-Inhalt. Der Zustand auf Modulebene wird auf dem Server ebenfalls über alle Anfragen hinweg geteilt, daher sollten Benutzerdaten daraus ausgeschlossen werden.
Warum Context nicht der sichere Kompromiss ist
Mit einem Modul-Store gibt es nichts zu liefern. Der state in themeStore.js befand sich niemals im Komponentenbaum; die Komponenten greifen darauf über einen Hook-Aufruf zu, ohne dass ein übergeordneter Provider beteiligt ist. Das Einhüllen von App in einen ThemeProvider würde eine Schicht hinzufügen, die nichts weiterleitet.
Context hat seine eigenen berechtigten Anwendungsbereiche: Das Einschränken eines Wertes auf einen Unterbaum oder das Ersetzen einer Abhängigkeit in Tests durch das Anzeigen eines anderen Providers. Oft wird er jedoch als vorsichtige Alternative zu einer Bibliothek empfohlen, und in dieser Rolle ist er teurer, als es scheint. Betrachten Sie einen einzigen Context, der sowohl ein Theme als auch einen Warenkorb enthält.
// context/AppContext.jsx
import { createContext, useState } from 'react';
const AppContext = createContext();
export function AppProvider({ children }) {
const [state, setState] = useState({
theme: 'light',
cart: ['book'],
});
return <AppContext.Provider value={state}>{children}</AppContext.Provider>;
}
export default AppContext;
Ein kleines Etikett liest daraus nur das Theme aus.
// components/ThemeLabel.jsx
import { useContext } from 'react';
import AppContext from '../context/AppContext';
function ThemeLabel() {
const { theme } = useContext(AppContext);
return <span>{theme}</span>;
}
export default ThemeLabel;
useContext(AppContext) registriert ThemeLabel beim gesamten Objekt, das der Provider enthält, und nicht nur bei theme. Wenn sich cart an einer anderen Stelle ändert, erhält der Provider ein neues state-Objekt, und da sich die Referenz ändert, wird ThemeLabel erneut gerendert – obwohl sich theme nicht geändert hat und die Komponente cart niemals berührt. Der Context bietet keine eingebaute Möglichkeit, anzuzeigen „wecke mich nur, wenn sich dieses Feld ändert“; jeder Verbraucher wird bei jeder Änderung des Wertes geweckt.
Der Store aus dem vorherigen Abschnitt vermeidet dies bereits. useTheme() liest einen einzigen Teilwert über seine getSnapshot-Funktion, und ein Komponente wird nur dann erneut gerendert, wenn der zurückgegebene Wert dieses Teilwerts sich ändert. Die Verwendung von Context als Zwischenlösung spart zwar eine Installation, führt aber zu häufigeren Neuerstellungen. Dies lässt sich mildern, indem der State in mehrere spezialisierte Contexts aufgeteilt wird – doch dann baut man im Grunde selbst das auf, was ein auf Selektoren basierender Store kostenlos bietet.
Wo State-Bibliotheken tatsächlich ihren Platz finden
Nichts davon macht Redux, Zustand oder MobX sinnlos. Die vier vorgebrachten Argumente haben versagt; die Bibliotheken hingegen nicht. Das Problem des „Prop-Drillings“ wird durch Context gelöst. Das Ändern von Zuständen erfolgt über useState. Automatische Aktualisierungen in unverwandten Komponenten werden durch useSyncExternalStore gelöst, das bereits mit React mitgeliefert wird. Und Context, der angebliche Kompromiss, erweist sich als aufwendiger zu handhaben als ein kleiner Store für gemeinsam genutzte, häufig ändernde Werte.
Ein echter Grund für den Einsatz einer Bibliothek besteht dann, wenn der gemeinsame Zustand von vielen unabhängigen Schreibern statt hauptsächlich Lesern verwaltet wird und wenn die Anzahl der konsumierenden Komponenten über das hinauswächst, was einige handgefertigte Module-Stores bewältigen können. Die Koordination von Aktualisierungen, Middleware, Entwicklertools sowie ein vorhersehbares Änderungsverfolgungssystem in diesem Umfang ist der Punkt, an dem sich eine ausgereifte Bibliothek lohnt – und sie verdient daher eine eigene Erklärung.
Kernpunkte
- Suchen Sie sich den Context, nicht einen Store, wenn das einzige Problem darin besteht, Werte durch Schichten zu übertragen, die sie nicht verwenden.
useStateist das richtige Werkzeug, um den Zustand einer Komponente zu ändern.- Für Zustände, die von Komponenten ohne gemeinsamen Elternteil geteilt werden, reicht oft ein kleiner Store, der auf
useSyncExternalStorebasiert. - Ein einziger, breit gefasster Context führt bei jeder Änderung zu einer Neulayoutung aller Nutzerkomponenten; für häufig ändernde Daten sollten enge Contexts oder ein auf Selektoren basierender Store bevorzugt werden.
- Nehmen Sie eine State-Bibliothek in Anspruch, wenn es viele unabhängige Schreiber und eine wachsende Anzahl von Lesern gibt – nicht wegen des Prop-Drilling-Problems.
Zusätzliche Literatur
- React-Prop-Drilling und „God Components“ auf ein angemessenes Maß reduzieren — Lernen Sie sieben konkrete Refactoring-Muster, um überdimensionierte React-Komponenten durch die Isolierung von State, Datenabruf, Berechtigungen und Ladelogik aufzuteilen, anstatt einfach nur Dateien zu splitten.
- Wie React-Redux entscheidet, wieder zu rendern: Selektoren, Gleichheit und typisierte Hooks — Ein Studienleitfaden zu den Interna von React-Redux: Provider, useSelector-Gleichheit, gememorierte Selektoren, connect(), typisierte Hooks mit withTypes() sowie seltene Randfälle.