React-Komponenten verbinden: Welcher Weg passt wann?

Wie React-Komponenten miteinander reden
Abstract
- #React-Komponenten verbinden
- #React-Kommunikation
- #Props, Context, Stores, Server State
- #React-Komponenten Datenaustausch
Datenaustausch zwischen React-Komponenten verständlich erklärt
Stellen Sie sich Ihre React-Anwendung für einen Moment wie ein großes Bürogebäude vor. In den einzelnen Räumen sitzen die Komponenten, und damit das Unternehmen funktioniert, müssen diese Räume miteinander reden. Mal sitzen zwei Kollegen Tür an Tür, mal liegen ihre Büros in verschiedenen Stockwerken, und manchmal liegt die eigentliche Information sogar in der Hauptverwaltung in einer ganz anderen Stadt.
Genau das ist die Herausforderung in React. Das Framework bietet Ihnen erstaunlich viele Wege, damit zwei Komponenten eine Information teilen. Welcher davon der richtige ist, hängt fast immer von zwei Fragen ab, über die viele Entwickler erstaunlich selten nachdenken: Wie weit sind die beiden Komponenten voneinander entfernt? Und welche Art von Wert wandert eigentlich zwischen ihnen hin und her?
In diesem Artikel gehen wir die Möglichkeiten der Reihe nach durch, beginnend mit dem kürzesten Weg.
Props und Callbacks: der kurze Dienstweg
Die einfachste Situation ist diese: Eine Eltern-Komponente rendert ein Kind, und beide müssen sich auf einen Wert einigen. Sie sitzen direkt nebeneinander im Komponentenbaum, es gibt also kaum Distanz zu überbrücken. Und für genau diesen Fall liefert React bereits alles mit, was Sie brauchen.
Die Daten fließen als Props von oben nach unten, und über Callbacks wieder nach oben. Sie können sich das wie einen Briefkasten vorstellen: Die Eltern stecken den Wert hinein, das Kind liest ihn, und über den Callback kann das Kind eine Antwort zurückschicken.
Ein Beispiel: Ein Filter gehört der Eltern-Komponente, und zwei Kinder interessieren sich dafür. Die Eltern-Komponente verwaltet den Zustand, eine Filterleiste liest und verändert ihn, und eine Liste nutzt ihn zur Anzeige. Der Zustand sollte dabei genau an der tiefsten Stelle leben, an der beide Kinder Zugriff darauf haben, also bei der gemeinsamen Eltern-Komponente. Dieses "Hochziehen des Zustands" ist der Standardweg, wenn Geschwister-Komponenten einen Wert teilen müssen.
Wenn der Wert durch viele Hände wandert
Schwierig wird es erst, wenn ein Wert durch Komponenten reisen muss, die sich überhaupt nicht für ihn interessieren, nur um endlich bei derjenigen anzukommen, die ihn wirklich braucht. Ein user-Objekt wird beispielsweise von Layout an Sidebar an Avatar durchgereicht, obwohl Layout und Sidebar gar nichts damit anfangen. Dieses Phänomen nennt man Prop Drilling, und es ist ein häufiger Grund, über andere Muster nachzudenken.
Doch bevor Sie zu schwererem Werkzeug greifen: Ein dreistufiges Prop Drilling lässt sich oft elegant durch Komposition auflösen. Statt die Daten nach unten durchzureichen, übergeben Sie das fertige Element als children. Der Wert wandert dann direkt dorthin, wo er gebraucht wird, und überspringt die uninteressierten Zwischenstationen einfach.
Colocation: braucht es die Kommunikation überhaupt?
Bevor Sie zwei Komponenten verbinden, lohnt sich eine ehrliche Frage: Müssen sie wirklich miteinander reden?
Oft wird ein Zustand "vorsorglich" nach oben geschoben, weil vielleicht irgendwann ein weiteres Kind ihn brauchen könnte. Taucht dieses Geschwister dann nie auf, wird der Zustand ohne Grund geteilt. Stellen Sie sich ein Dashboard vor, in dem ein Suchbegriff von zwei Kindern genutzt wird, ein isMenuOpen-Schalter aber nur vom Menü selbst. Der Suchbegriff gehört nach oben, der Menü-Schalter dagegen nicht: Lebt er trotzdem in der Eltern-Komponente, rendern das Dashboard und alle seine Kinder unnötig neu, sobald sich das Menü öffnet.
Colocation bedeutet, eine Komponente und ihren Zustand so nah wie möglich an die Stelle zu rücken, an der sie tatsächlich gebraucht werden, und niemals höher als nötig. Das macht den Code nicht nur klarer, sondern verbessert ganz nebenbei die Performance, weil weniger Komponenten überflüssig neu rendern. Aus meiner Erfahrung war ein guter Teil des "geteilten Zustands", den ich über die Jahre gelöscht habe, in Wahrheit nie von irgendetwas geteilt worden.
Imperative Aufrufe: "Tu mal eben das hier"
Manchmal will eine Eltern-Komponente überhaupt keinen Zustand mit einem Kind teilen. Sie möchte stattdessen hineingreifen und dem Kind sagen, es soll etwas tun: dieses Video abspielen, dieses Eingabefeld fokussieren, zu dieser Zeile scrollen, diesen Dialog öffnen.
Das passende Werkzeug dafür ist eine Ref in Kombination mit useImperativeHandle. Das Kind legt eine kleine, bewusst gewählte Auswahl an Methoden offen, und die Eltern dürfen diese direkt aufrufen. In React 19 ist ref endlich ein ganz normales Prop, das Kind kann es also direkt aus seinen Props lesen. Wer noch React 18 oder älter nutzt, muss die Komponente lediglich in forwardRef einpacken, alles andere bleibt identisch.
Die Faustregel, wann Sie dieses Werkzeug brauchen, ist herrlich einfach: Würden Sie natürlicherweise sagen "tu X", dann ist es imperativ und das ist Ihr Werkzeug. Würden Sie sagen "wir müssen beide Y kennen", dann handelt es sich um Zustand.
Context: Werte für das ganze Gebäude
Jetzt ist die Distanz zwischen den Komponenten wirklich gewachsen. Ein Wert wird von vielen Komponenten in ganz unterschiedlichen Tiefen gebraucht, der Kompositions-Trick reicht nicht mehr, und alles um children herum umzubauen würde Ihren Baum unleserlich machen. Obendrein ändert sich der Wert, einmal gesetzt, kaum noch.
Die Klassiker sind hier das aktuelle Theme, der angemeldete Benutzer, die aktive Sprache oder die Akzentfarbe Ihres Design-Systems. Werte also, die überall gelesen, aber fast nie geschrieben werden. Genau dafür wurde Context gebaut. Jede Komponente unter dem Provider kann mit useContext den Wert direkt greifen, ohne ihn durch jede Zwischenebene fädeln zu müssen. Sie können sich Context wie die Lautsprecheranlage im Bürogebäude vorstellen: Eine Durchsage erreicht jeden Raum, ganz egal in welchem Stockwerk.
Die Performance-Falle von Context
Und genau hier liegt der Haken, der Context seinen leicht ramponierten Ruf eingebracht hat. Wann immer sich der Wert im Context ändert, rendert jede konsumierende Komponente neu. Alle, ohne Ausnahme. Und nein, React.memo rettet Sie hier nicht, was viele überrascht. memo überspringt ein Re-Rendering nur, wenn sich die Props nicht ändern. Ein Context-Konsument reagiert aber gar nicht auf Props, er greift an ihnen vorbei und abonniert den Context direkt.
Besonders tückisch wird es, wenn Sie ein Objekt direkt als value übergeben. Bei jedem Render der App entsteht ein brandneues Objekt mit neuer Identität, selbst wenn sich kein einziger Wert darin geändert hat. So funktionieren JavaScript-Objekte nun einmal. React vergleicht, sieht eine andere Identität und zwingt jedes Kind zum Neurendern.
Es gibt Auswege: Sie können den einen Context in mehrere schmale aufteilen, den Wert in useMemo verpacken oder den Zustands-Context vom Dispatch-Context trennen. Doch sobald Sie anfangen, gesplittete Contexts und useMemo zu stapeln, um Selektor-Verhalten von Hand nachzubauen, das Context von Natur aus nicht bietet, ist klar: Context war für langsam wechselnde Werte gedacht, nicht als Datenspeicher für einen Zustand, der sich über einen weiten Baum mehrmals pro Sekunde ändert. Wenn Sie so hart dagegen ankämpfen, sind Sie ihm entwachsen.
Ein globaler Store: für echt globalen Zustand
Damit landen wir bei der Situation, die Context nicht mehr bewältigt. Der Wert ändert sich häufig, viele Komponenten lesen jeweils ein anderes Stück davon, und jede soll nur dann neu rendern, wenn sich ihr eigenes Stück verändert. Das ist der Moment für eine State-Management-Bibliothek.
Meine erste Wahl ist heute meist Zustand, vor allem wegen seiner geringen Größe. Eine Komponente abonniert mit einem Selektor genau das Stück, das sie interessiert, etwa die Anzahl der Artikel im Warenkorb. Zustand weckt sie dann nur, wenn sich genau diese Zahl ändert. Da der Store nicht an Reacts Render-Zyklus hängt, können Sie ihn sogar von außerhalb einer Komponente lesen, was praktisch ist, sobald Sie Store-Logik aus einem Event-Handler oder einer Hilfsfunktion heraus aufrufen wollen.
Ein wichtiger Hinweis: Schreiben Sie keinen Selektor, der bei jedem Aufruf ein neues Objekt oder Array zurückgibt, sonst handeln Sie sich genau dasselbe Identitätsproblem wie beim globalen Context ein. Hier hilft der Hook useShallow, der die Inhalte statt der Identität vergleicht.
Zustand ist nicht die einzige Option. Redux Toolkit bleibt die richtige Wahl für große, langlebige Anwendungen mit vielen Entwicklern, bei denen die Zeitreise der DevTools echten Wert hat. Und Jotai modelliert Zustand als kleine Atome, die Komponenten einzeln abonnieren. Die Regel hinter allen dreien ist dieselbe: Ein globaler Store ist für Zustand, der ehrlich gesagt wirklich global ist. Der häufigste Fehler ist, die Reichweite des Stores als Abkürzung für Zustand zu missbrauchen, der überhaupt nicht global ist.
Server State: die Information gehört woanders hin
Hier kommt eine Erkenntnis, die mich peinlich lange gekostet hat: Der größte Teil dessen, was wir "geteilten Zustand" nennen, gehört der Anwendung gar nicht. Er gehört dem Server, und Ihre App hält lediglich eine Kopie.
Denken Sie an ein Benutzerobjekt, eine Bestellliste oder den Inhalt eines Warenkorbs, der in Wahrheit in einer Datenbank liegt. Packt ein Team all das in einen Store, muss es eine ganze Maschinerie drumherum von Hand bauen: Lade-Flags, Fehler-Flags, Nachladen, Cache-Invalidierung. Server State verhält sich nämlich grundlegend anders als Client State. Er gehört woanders, veraltet von selbst, kann von anderen verändert werden, während Sie ihn ansehen, und zwischen Anfrage und Antwort liegt immer eine Verzögerung.
Genau hier kommt TanStack Query ins Spiel. Sie legen einmal an der Wurzel einen QueryClient an, und jeder useQuery-Hook im Baum liest aus diesem gemeinsamen Cache. Kein eigenes useState für die Daten, kein useEffect für den Abruf, kein manuelles Lade-Flag. Und das Schöne für die Kommunikation: Rufen zehn verteilte Komponenten dieselbe Query mit demselben Schlüssel auf, feuert nur eine einzige Netzwerkanfrage, und alle aktualisieren sich gemeinsam, sobald sich der Eintrag ändert. Schreibvorgänge laufen über Mutationen, die dem Cache mitteilen, welche Schlüssel nun veraltet sind. Sobald Ihr Server State dort lebt, wo er hingehört, schrumpft Ihr globaler Store oft auf fast nichts zusammen.
URL State: die Adresszeile als gemeinsamer Speicher
Vieles, was wir Anwendungszustand nennen, beschreibt eigentlich nur, was der Benutzer gerade ansieht. Der aktive Filter, die Seite einer Tabelle, die Suchanfrage, der gewählte Tab. Für diese ganze Kategorie hat Ihnen der Browser längst einen globalen, teilbaren und beständigen Speicher geschenkt: die URL.
Sobald dieser Zustand in der URL lebt, lösen sich drei Probleme von selbst. Die Ansicht wird teilbar, weil der Link den Zustand mitträgt. Sie übersteht einen Reload, weil sie nie nur im Speicher gefangen war. Und der Zurück-Button funktioniert wieder wie erwartet. In der Praxis greife ich zu nuqs, das einen Suchparameter wie ein typisiertes useState verhält. Eine Filterleiste und eine Ergebnistabelle weit voneinander entfernt bleiben so im Gleichschritt, ohne gemeinsame Eltern, Context oder Store. Die Faustregel: Beschreibt der Zustand die Ansicht und würde ein Benutzer ihn verlinken wollen, gehört er in die URL. Ein halb geöffnetes Dropdown dagegen bleibt lokal.
Event-getriebene Kommunikation: einfach in den Raum rufen
Zum Schluss der Fall zweier Komponenten ohne sinnvollen gemeinsamen Vorfahren, ohne geteilten URL-Zustand, ohne gemeinsame Server-Ressource. Ihre Beziehung lautet nicht "wir müssen beide diesen Wert kennen", sondern "wenn hier etwas passiert, soll dort etwas reagieren".
Das Paradebeispiel ist ein Toast-Benachrichtigungssystem. Fast überall in Ihrer App könnte ein Toast ausgelöst werden, während ein einzelner Anzeigebereich im Layout für die Darstellung zuständig ist. Ein Toast ist aber kein Wert, sondern ein Moment: Er flammt auf, wird gezeigt und ist wieder weg. Context und Stores sind für beständige Werte gebaut, sodass Sie das Werkzeug regelrecht spüren, wenn Sie ein flüchtiges Ereignis hineinzwängen.
Das passende Muster heißt Publish-Subscribe: Ein Sender ruft "das ist passiert" in den Raum, ohne zu wissen, wer zuhört, und die Empfänger reagieren, ohne zu wissen, wer gerufen hat. Der Browser bringt dafür mit EventTarget bereits alles mit, oder Sie bauen sich in einem Dutzend Zeilen einen eigenen kleinen Event-Bus. Diese totale Entkopplung ist großartig, aber auch heikel: Beim Debuggen können Sie dem Fluss nicht mehr durch den Baum folgen, und ein Ereignis, das vor seinem Empfänger feuert, ist einfach verloren. Behandeln Sie das daher als echten letzten Ausweg für quer schneidende "Feuern und vergessen"-Signale.
Die richtige Wahl treffen
Wenn man alles zusammendampft, kommt die Entscheidung jedes Mal auf dieselben zwei Fragen zurück: Wie weit sind die Komponenten voneinander entfernt, und welche Art von Wert bewegt sich zwischen ihnen?
Die meisten Apps brauchen nur eine kleine Handvoll dieser Werkzeuge. Props und Callbacks ständig, ein wenig Context für wirklich app-weite Dinge wie Theme und Benutzer, und TanStack Query für alles, was vom Backend kommt. Diese Kombination trägt Sie erstaunlich weit. Noch nie ist ein Dashboard zum Stillstand gekommen, weil jemand Props benutzt hat. Es ist immer die andere Richtung: ein globaler Store für lokalen Zustand, ein Context, der Server-Daten hütet, ein Event-Bus, der einen schlichten Wert schmuggelt.
Fazit
Die großen Werkzeuge sind so verführerisch, weil sie von überall aus zugreifen können. Doch "greift von überall" ist verdächtig schwer von "ich habe nie darüber nachgedacht, wo das eigentlich hingehört" zu unterscheiden. Greifen Sie deshalb stets zum nächstgelegenen Werkzeug, das das Problem überhaupt erreichen kann, und bewegen Sie sich erst dann nach außen, wenn dieses Werkzeug wirklich an seine Grenzen stößt. Wer sich diese eine Haltung angewöhnt, schreibt fast automatisch aufgeräumteren und schnelleren React-Code.
Häufig gestellte Fragen
Wann sollte ich Context und wann einen globalen Store wie Zustand verwenden?
Context eignet sich für Werte, die selten wechseln und überall gelesen werden, etwa Theme oder angemeldeter Benutzer. Sobald sich der Wert häufig ändert und viele Komponenten jeweils nur ein kleines Stück davon brauchen, ist ein Store wie Zustand die bessere Wahl, weil dort jede Komponente nur dann neu rendert, wenn sich ihr eigenes Stück verändert.
Warum gehört Server State nicht in meinen normalen Zustand?
Server State gehört dem Server, nicht Ihrer App, die nur eine Kopie hält. Diese Kopie veraltet von selbst und kann von anderen verändert werden. Werkzeuge wie TanStack Query übernehmen Caching, Nachladen und Fehlerbehandlung automatisch, sodass Sie diese aufwendige Logik nicht von Hand und meist fehleranfällig nachbauen müssen.
Ist Prop Drilling immer ein Problem?
Nein. Ein Wert, der nur eine oder zwei Ebenen tief gereicht wird, ist völlig in Ordnung. Erst wenn er durch viele Komponenten wandert, die ihn gar nicht nutzen, lohnt sich Gegensteuern, oft schon durch Komposition mit children, bevor Sie zu Context oder einem Store greifen.
- Technologien
- Programmiersprachen
- Tools