Startseite / Artikel

Verwenden Sie React Context für ein gemeinsames Umfeld, nicht als Ablagekorb.

Der Kontext verteilt Werte über einen Baum; er definiert weder Eigentumsverhältnisse noch aktualisiert Grenzen oder ein Zustandsmodell. Verwenden Sie lieber enge Anbieter sowie echte Tools für den Serverzustand.

Verlag: REACTAPP.TOP10 Min. Lesezeit2308 Wörter

Quellen: Most Developers Use React Context for the Wrong Problems

Prop-Drilling ist nicht immer ein architektonisches Versagen

React Context dient oft als schnelle Lösung. Ein Wert wird an drei Komponenten weitergegeben, die ihn nie verwenden. Ein weiteres Bildschirm benötigt den aktuellen Benutzer. Ein Modal muss tief im Baum geöffnet werden. Das Weiterleiten von Props wird mühsam, daher taucht ein Provider auf und der Baum sieht plötzlich ordentlicher aus.

Zwischenkomponenten geben ungenutzte Props ab. Tiefere Kinderkomponenten erhalten über Hooks nur das, was sie benötigen. Die Struktur wirkt bewusster als das Durchleiten desselben Arguments in jeder Schicht.

Dieser ästhetische Vorteil kann eine wichtigere Frage verdecken: Gehört der Wert überhaupt in Context?

Der Kontext löst ein Problem: Ein Elternteil kann einen Wert an Nachkommen veröffentlichen, ohne ihn durch jede Zwischendatei leiten zu müssen. Er entscheidet jedoch nicht darüber, wo der Zustand gespeichert werden soll, wie oft die Verbraucher aktualisiert werden müssen, ob unverwandte Werte einen gemeinsamen Speicher teilen sollen oder welche Komponente ein Workflow besitzt.

Diese Entscheidungen bleiben bestehen. Der Kontext kann sie verbergen, da der Zugriff unsichtbar wird. Ein Kind kann Daten lesen, ohne sie in den Props zu deklarieren. Ein Provider kann unverwandte Felder ansammeln, weil alles als „global“ wahrgenommen wird. Eine kleine Aktualisierung kann sich auf Dutzende von Verbrauchern ausbreiten, die denselben Kontext-Objekt teilen. Ein scheinbar wiederverwendbares Widget kann von einem Provider abhängen, der nur auf einer Seite existiert.

Die Schwäche liegt nicht in der API. Die Schwäche besteht darin, einfachen Zugriff statt klarer Eigentumsverhältnisse zu verwenden.

Prop-Drilling hat einen schlechten Ruf, weil die Wiederholungen sichtbar sind. Ein Wert beginnt hoch, durchquert Layout-Komponenten und erreicht schließlich das Element, das ihn benötigt. Die mittleren Schichten sehen aus wie Leitungen, wodurch Context wie eine Verbesserung erscheint.

Mannchmal sind diese Wiederholungen nützliche Informationen. Sie zeigen den Ursprung an, welche Abteilung vom Wert abhängt und wie weit die Abhängigkeit reicht. Props machen Eingaben suchbar. Wenn sich der Wert ändert oder verschwindet, bleibt die Auswirkung lokal und offensichtlich.

Context löscht diesen Pfad aus. Ein Nachkomme kann mit dem Lesen beginnen, ohne die Zwischenverträge aktualisieren zu müssen. Der Baum hängt weiterhin vom Anbieter ab, doch die Beziehung erscheint nicht mehr auf der Komponentenoberfläche.

Das bedeutet nicht, dass jede lange Prop-Kette beibehalten werden muss. Wenn ein großer Unterbaum tatsächlich nur eine Sache gemeinsam hat, wird das Verknüpfen von Daten zu Überfluss. Der Fehler besteht darin, jedes wiederholte Prop als Beweis dafür zu betrachten, dass ein Kontext fehlt.

Mannchmal liegt der Wert zu hoch, weil die Zugehörigkeit falsch ist. Eine Seite enthält einen Entwurf eines Dialogs und leitet ihn über mehrere Ebenen zu einem Formular weiter. Ein Kontext würde den Zugriff erleichtern, doch ein stärkeres Design könnte es dem Formular oder Dialog erlauben, den Entwurf zu verwalten.

Mannchmal beseitigt die Komposition den mittleren Schritt. Ein Elternelement kann das Element erstellen, das die Daten benötigt, und dieses Element als children in ein Layout übergeben. Das Layout erfährt nie von dieser Abhängigkeit.

Wiederholte Props sind ein Symptom. Ein Kontext ist nur eine Lösungsmöglichkeit und nicht die Standarddiagnose.

Kontext wandelt lokalen Zustand nicht sicher in globalen Zustand um

Ein häufiges Muster besteht darin, immer einen Kontext zu erfinden, wenn mehrere Komponenten denselben Zustand aktualisieren müssen. Der Anbieter speichert Werte, stellt Setter bereit, und jede Nachkomponente kann lesen oder schreiben.

Oft beginnt es klein:

const AppContext = createContext(null);

function AppProvider({ children }) {
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [activeModal, setActiveModal] = useState(null);
  const [selectedRecord, setSelectedRecord] = useState(null);
  return (
    <AppContext.Provider
      value={{
        sidebarOpen,
        setSidebarOpen,
        activeModal,
        setActiveModal,
        selectedRecord,
        setSelectedRecord,
      }}
    >
      {children}
    </AppContext.Provider>
  );
}

Der Zugriff befindet sich an einem Ort, doch die Bedeutung des Zustands bleibt weiterhin unklar. Die Sichtbarkeit eines Sidebars, welche Modalleiste geöffnet ist und welches Record ausgewählt wird, gehört oft zu separaten Funktionen und wird zu unterschiedlichen Zeiten umgesetzt.

Jedes Kind im Baum kann anschließend den Speicher des Anbieters direkt überschreiben. Eine Tabelleingabe kann eine Auswahl vornehmen, eine Modalleiste öffnen und den Sidebar schließen. Komponenten drücken keine Absichten aus; sie greifen auf gemeinsame Felder zu und bilden die gewünschte Kombination zusammen.

Danach folgt die unsichtbare Kopplung. Das Modul geht davon aus, dass bereits eine Auswahl vorhanden ist. Die Tabelle nimmt an, dass das Schließen des Moduls die Auswahl löscht. Die Seitenleiste reagiert auf einen Workflow, den sie nicht kontrolliert. Diese Beziehungen werden weder in einem Reducer noch in einer Operation noch in einer Funktionsgrenze erfasst – sie existieren nur, weil viele Komponenten viele Setter aufrufen können.

Das Hervorheben von Feldern in den Kontext verschiebt lediglich die Speicherung. Klarheit der Bedeutung entsteht dadurch nicht automatisch.

Ein stärkerer Anbieter stellt Operationen statt roher Setter zur Verfügung – und zwar nur für ein zusammenhängendes Anliegen. Ein Dialogsystem könnte openDialog und closeDialog anbieten. Ein Authentifizierungsanbieter könnte Sitzungsdaten sowie Anmeldeoperationen bereitstellen. Ein Theme-Anbieter könnte das aktuelle Thema sowie einen gezielten Aktualisierer bereitstellen.

Halten Sie nicht bei dem Gedanken an „mehrere Komponenten benötigen Zugriff“ inne. Fragen Sie stattdessen, ob sie denselben Lebenszyklus und dasselbe Anliegen teilen.

Ein Kontextobjekt kann eine enorme Aktualisierungsfläche schaffen

Kontexte verteilen außerdem Aktualisierungen. Wenn sich der Wert des Anbieters ändert, können die Verbraucher dieses Kontexts neu rendern. Das ist in Ordnung, wenn der Wert ein einzelnes Thema darstellt und die Verbraucher tatsächlich von seinen Änderungen abhängen.

Probleme entstehen, wenn ein Anbieter Werte mit unzusammenhängenden Aktualisierungsfrequenzen zusammenfasst.

function ApplicationProvider({ children }) {
  const value = {
    user,
    theme,
    locale,
    notifications,
    sidebarOpen,
    featureFlags,
    liveJobStatus,
  };

  return (
    <ApplicationContext.Provider value={value}>
      {children}
    </ApplicationContext.Provider>
  );
}

Ein Schalter für ein Theme sollte keine Tabelle ungültig machen, die nur das ausgewählte Datensatz benötigte. Ein Flag für geöffnete Seitenleiste sollte kein Formular aktualisieren, das nur die Benutzer-ID benötigte. Das Zusammenfassen dieser Felder in einem Objekt macht jeden Verbraucher empfindlich gegenüber jeder Schreiboperation – es sei denn, man speichert sorgfältig und teilt die Kontexte auf.

Teams bemerken manchmal erst nach der Profilierung die Kosten. Bis dahin haben bereits viele Funktionen den Mega-Hook importiert. Das Aufteilen erfordert es, jeden Verbraucher zu finden und festzulegen, welchen Teil jeder tatsächlich benötigte.

Wählen Sie lieber schmale Anbieter, die mit dem Update-Rhythmus übereinstimmen. Legen Sie den UI-Chrome mit hohem Wechselkoeffizienten an einem Ort und die stabilen Sitzungsdaten an einem anderen. Speichern Sie die Werte der Anbieter im Cache, wenn sich die Objektidentität sonst bei jeder Darstellung ändern würde. Messen Sie vor dem Hinzufügen „nur eines weiteren Feldes“ zu einer bereits vorhandenen Struktur.

Der Kontext kann fehlende Komponentengrenzen verbergen

Wenn es schwierig ist, eine Funktion zu verbinden, kann der Kontext fehlende Grenzen überdecken. Ein Produkteditor, der stumm den aktiven Produktzustand, Entwurf, Validierung, Berechtigungen sowie den Mutationszustand aus dem Seitenkontext abruft, macht nur innerhalb dieser einen Seite Sinn.

Einige Abhängigkeiten sind angemessen implizit, da sie das Umfeld statt Feature-Daten beschreiben – Theme, Lokalisierung, Routing, ein gemeinsamer HTTP-Client. Solche sind in der Regel umfassend und relativ stabil.

Die Eigenschaftendaten sind unterschiedlich. Ihr Verstecken in Context beseitigt unnötigen „Prop“-Lärm, erhöht aber gleichzeitig die architektonische Abhängigkeit. Fehlende Props bedeuten nicht unbedingt fehlende Abhängigkeiten; oft bedeutet es lediglich, dass die Abhängigkeiten aus dem Sichtfeld geraten sind.

Fragen Sie sich, ob das Komponentenkonzept außerhalb des aktuellen Providers verstanden und getestet werden kann. Wenn die Antwort nein lautet, könnte Context versuchen, eine Grenze auszugleichen, die eigentlich durch einen klareren Props- oder Kompositionsvertrag definiert werden sollte.

Context wird oft anstelle einer ordnungsgemäßen Server-Zustandsverwaltung verwendet

Eine weitere Fehlnutzung besteht darin, geladene Daten in Context zu packen, weil mehrere Komponenten sie benötigen. Der Provider ruft einmal an, speichert die Antwort und stellt anschließend Daten, Ladezustände, Fehler sowie Funktionen zum Erneuern zur Verfügung. Es sieht aus wie eine gemeinsame Datenschicht – ohne zusätzliche Bibliotheken.

Der Zustand eines echten Servers erfordert schnell weitere Funktionen: Entduplizierung von Anfragen, Regeln zur Vermeidung veralteter Daten während der Erneuerung, Wiederholungsversuche, Stornierung, Paginierung, Aktualisierungen nach einer Änderung sowie die Beibehaltung sichtbarer vorheriger Daten beim Erneuern.

Der Kontext bietet nichts davon. Er leitet lediglich das weiter, was der Anbieter berechnet hat.

Der Anbieter wird allmählich zu einer manuell verwalteten Cache-Lösung. Komponenten sind von den Besonderheiten seiner Erneuerungsmechanismen abhängig. Änderungen beheben manuell gemeinsam genutzte Arrays. Verschiedene Anbieter entwickeln leicht unterschiedliche Mechanismen für das Laden und die Fehlerbehandlung. Das Verlassen einer Route kann dazu führen, dass der Cache verloren geht; das Hinzufügen zweier Anbieter kann zu doppelten Abfragen führen.

Eigens entwickelte Datenbibliotheken existieren, weil der Zustand in der Ferne ein anderes Lebenszyklusverhalten aufweist als der lokale UI-Zustand: Eigentumsrechte am Server, Aktualität, Caching, Invalidation, Konkurrenz und Fehlermöglichkeiten.

Context kann einen konfigurierten Client oder eine stabile Abhängigkeit von Funktionen bereitstellen. Es ist selten der richtige Ort, um die Anfragerkoordination mit gewöhnlichen useState und useEffect neu aufzubauen. Ein fokussierter Provider, der Serverdaten mit einem bestimmten Workflow koordiniert, kann weiterhin sinnvoll sein, doch die globale Zugänglichkeit der Daten löst das Problem der Synchronisierung nicht.

Die Zugänglichmachung eines Payloads ist keine gleichwertige Vorgehensweise wie das Verwalten von Remote-Daten.

Context ersetzt kein Zustandsmodell

Wenn ein Workflow komplex wird, fügen Teams ihn manchmal in Context ein, damit jeder Schritt auf dieselben Felder zugreifen kann. Prozesse wie Checkout, Onboarding, Editor-Funktionen sowie mehrstufige Formulare enden oft auf diese Weise.

Das funktioniert, wenn der Provider einen bestimmten Workflow darstellt und kontrollierte Übergänge ermöglicht. Es wird jedoch instabil, wenn Context lediglich eine Sammlung von Feldern und Settern ist.

Ein Zahlungsdienstleister kann Felder für die Adresse, Zahlungsangaben, Lieferoptionen, den aktuellen Schritt, Validierungsfehler, Ladeindikatoren, Modalfenster-Flags, Gesamtbeträge sowie zahlreiche Setter enthalten. Jedes Kindobjekt kann einen beliebigen Teil unabhängig verändern.

Das Teilen von Speicher ist nicht dasselbe wie das Definieren von Regeln. Ein Wechsel des Landes kann eine Anpassung der Lieferoptionen erfordern, während ein Wechsel der Lieferart eine Neuberechnung der Gesamtbeträge notwendig machen kann. Das Zurückgehen im Prozess kann dazu führen, dass einige Felder erhalten bleiben und andere ungültig werden. Ein Zahlungsausfall kann den Ablauf verändern, ohne die Adresse zu löschen.

Diese Beziehungen erfordern ein Zustandsmodell – Reduzierer oder explizite Operationen, die gültige Übergänge kodieren. Der Kontext kann dieses Modell verteilen; er ist jedoch nicht das Modell selbst.

Solange es keine Regeln gibt, tauchen unmögliche Kombinationen auf: Zahlungsschritt ohne gültige Lieferung, sowohl Erfolg als auch Fehler als wahr markiert, Lieferoptionen, die zu einer früheren Adresse gehören. Man benötigt nicht unbedingt eine formelle Zustandsmaschinen-Bibliothek. Dennoch muss klar definiert werden, welche Fakten für sich stehen, welche Kombinationen zulässig sind und welche Ereignisse mehrere Felder bei einem Übergang überschreiben.

Durch den Kontext wird ein gutes Modell erreichbar. Er kann ein verwirrendes Modell nicht einfach dadurch retten, dass er es höher im Baum platziert.

Globaler Zugriff kann das Eigentumsgefühl schwächen

Die Werte im Kontext sind leicht lesbar und von vielen Orten aus einfach einfügbar. Der Komfort kann das Eigentumsgefühl untergraben.

Ein Benachrichtigungskontext, der durch Netzwerkhandler, Formulare, Seiten und Hintergrundaufgaben aktualisiert wird. Ein Auswahlkontext, der durch Tabellen, Dialogfelder, Schnellzugriffe und Routenänderungen verändert wird. Ein Einstellungenkontext, der direkt von jeder Steuerung geschrieben wird, die eine Einstellung anzeigt.

Wenn viele Entwickler denselben Speicher teilen, verschwimmt die Verantwortung für die Richtigkeit.

Eine stärkere Verantwortung entsteht durch eingeschränkte Operationen. Wählen Sie addNotification, dismissNotification und clearExpiredNotifications statt eines einfachen setNotifications. Wählen Sie signIn, signOut und refreshSession statt eines generischen setSession. Solche Operationen haben eine Bedeutung und ermöglichen es dem Anbieter, Invarianten beizubehalten.

Auch dann sollte der Anbieter fokussiert bleiben. Ein universeller Dispatcher für jede Funktion schafft nur eine andere Bezeichnung für globale Kopplungen.

Viele Komponenten können den aktuellen Benutzer abrufen. Nur wenige sollten die Sitzung ersetzen. Der Kontext bleibt wartbar, wenn der Anbieter das Lebenszyklusmanagement übernimmt und die Nutzer über einen einfachen Vertrag kommunizieren.

Ein Context Provider kann zu einem versteckten Monolithen werden

Überdimensionierte Komponenten machen sich durch ihre Dateilänge bemerkbar. Überdimensionierte Anbieter bleiben länger ruhig.

Das System kann modular erscheinen – mit kleinen Komponenten und Hooks – während ein einziger Anbieter den Anwendungsstatus, Netzwerkaufrufe, die Persistenz, Browserereignisse, Berechtigungen, Benachrichtigungen, Modale sowie abgeleitete Werte verwaltet. Die Komponentendateien bleiben kurz, weil der Anbieter die Koordination übernimmt.

Das ist dennoch ein Monolith; er generiert einfach nur wenig JSX.

Der Anbieter ändert sich aus unzusammenhängenden Gründen. Benachrichtigungsregeln, Themenanforderungen, Sitzungswechsel sowie Modalaufgaben ändern alle dasselbe Modul. Jeder Verbraucher hängt von seiner Stabilität ab. Die Tests werden umfangreicher, da die Operationen über gemeinsamen Zustand miteinander interagieren. Die Leistung leidet, wenn Updates sich ausbreiten. Refaktorisierungen zwingen dazu, Annahmen, die in einem überall verwendeten Wert verankert sind, zu entwirren.

Die Lösung besteht nicht darin, unnötig viele Neststrukturen zu verwenden. Nestungen sind akzeptabel, wenn jeder Anbieter eine klare Verantwortung hat. Einige fokussierte Anbieter sind in der Regel klarer als ein Objekt, das vorgibt, einen einzigen Lebenszyklus zu teilen.

Auch die Platzierung ist wichtig. Ein Kontext, der von einem bestimmten Pfad benötigt wird, sollte diesen Unterbaum umfassen – nicht die gesamte Anwendung. Eine eng gefasste Platzierung verringert sowohl die Abhängigkeiten als auch den Umfang der Aktualisierungen.

Globale Verfügbarkeit sollte erarbeitet werden, nicht als Standardort für unpraktische Eigenschaften dienen.

Kontexte eignen sich hervorragend für die Probleme, die sie tatsächlich lösen

Kontexte sind besonders nützlich, wenn ein Unterbaum tatsächlich eine gemeinsame Umgebungsabhängigkeit oder ein stabiles Problem teilt.

Themen eignen sich, weil viele Nachkommen dieselbe Präsentationsauswahl benötigen. Lokalisierung eignet sich, weil Formatierung und Übersetzung weitreichend angewendet werden können. Ein gemeinsamer API-Client, die Routing-Umgebung, ein Form-Bereich oder ein einziger Workflow können ebenfalls dazugehören, wenn alle Nutzer innerhalb einer Grenze liegen.

Der Wert muss nicht statisch sein. Er sollte eine kohärente Bedeutung haben sowie ein Aktualisierungsmodell, das den Nutzern entspricht.

Ein gut gestalteter Context beantwortet: In welcher Umgebung läuft dieser Unterbaum ab, welche Abhängigkeiten teilen diese Komponenten und zu welchem fokussierten Workflow gehören sie?

Ein schlecht gestalteter Context beantwortet: Welche Werte waren lästig, als Eigenschaften übergeben zu müssen? Diese Frage führt zu unzusammenhängendem Zustand, weiten Aktualisierungsflächen, versteckten Abhängigkeiten und rohen Settern. Die Verbindungen verschwinden, während die Zuordnung der Verantwortlichkeiten schwerer erkennbar wird.

Vor dem Hinzufügen eines Contexts sollten Sie prüfen, ob ein sinnvoller Unterbaum denselben Wert teilt, ob die Nutzer einen gemeinsamen Lebenszyklus haben, wie oft sich der Wert ändert und ob der Anbieter einen eng gefassten und stabilen Vertrag veröffentlichen kann. Fragen Sie außerdem, welches Problem der Context verbergen würde. Falsch platzierte Zustände, fehlende Funktionsgrenzen, schwache Komposition, Caching von Server-Zuständen oder ein undefinierter Workflow lassen sich durch einfacheren Zugriff nicht beheben.

Context sollte Entscheidungen verteilen, sie nicht vermeiden

Die meisten Fehlbetrachtungen entstehen nicht durch ein Missverständnis der API, sondern durch sofortige Erleichterung. Die Prop-Chain verschwindet, Komponenten erhalten Zugriff, und eine Funktion wird ohne Neuordnung des Baums bereitgestellt. Der heutige Code ist einfacher zu schreiben.

Die Kosten entstehen, wenn der Zugriff die Eigentumsverhältnisse übersteigt. Unzusammenhängende Lebenszyklen teilen sich denselben Anbieter. Nutzer erneuern die Darstellung aufgrund von Änderungen, die sie ignorieren. Funktionskomponenten sind von versteckten Umgebungen abhängig. Die Anbieter verwandeln sich stillschweigend in improvisierte Speicher, Anfragecaches und Workflow-Engines, die niemals ein explizites Modell erhalten haben.

Context als schlechte Zustandsbibliothek zu bezeichnen, verfehlt den Kernpunkt. Sie hat niemals behauptet, Zustandsverwaltung definieren zu können; sie überträgt lediglich einen Wert den Baum hinab.

Das umgebende Design muss weiterhin beantworten, was dieser Wert darstellt, welches Modul ihn besitzt, wer seine Änderung erlaubt ist, wie häufig Updates ausgelöst werden und welche Nutzer tatsächlich zu ihm gehören.

Context ist am stärksten, nachdem diese Entscheidungen bereits klar sind.