Ein mentales Modell für React erstellen: Vereinbarkeit, Zustand und Hooks
Erfahren Sie die Gründe hinter den Kernkonzepten von React – Reconciliation, Komponenten, Props, State und Hooks – um Intuition zu entwickeln anstelle von API-Auswendiglernen.
Falls Sie bereits Code schreiben können, aber noch nicht tief in React eingedrungen sind – oder nur kurz damit experimentiert haben –, ist dieser Artikel speziell für Sie geschrieben.
Wenn Sie mit dem Lernen von React beginnen, ist es verlockend, direkt zu useState, useEffect, Props, Hooks sowie einer langen Liste weiterer APIs überzugehen. Sie können die Syntax erlernen, etwas zum Laufen bringen – ohne jedoch wirklich zu verstehen, warum React sich so verhält.
Dieser Artikel soll diese Lücke schließen.
Anstatt React als eine Ansammlung von APIs zu betrachten, die man auswendig lernen muss, werden wir die Gründe hinter Reacts Design und wie die einzelnen Bestandteile zusammenpassen untersuchen. Die Syntax ist wichtig, doch sie fällt viel leichter zu verstehen, sobald man weiß, was dahinter vor sich geht. Deshalb beginnen wir nicht mit wie man React-Code schreibt, sondern erst einmal damit, wie man über React nachdenkt.
Wir werden Komponenten, Props, Zustand, Rendering, Reconciliation, Hooks, Prop-Drilling, Context und Routing behandeln und jede Idee mit dem Problem verbinden, das sie lösen soll. Ziel ist es nicht, alle APIs zu listeten, die React bietet, sondern Ihnen ein mentales Modell zu vermitteln, das diesen APIs beim ersten Kontakt hilft, verständlich zu werden.
Sie haben wahrscheinlich schon gehört, dass ein cleverer Algorithmus dazu beiträgt, warum React so schnell ist. Das klingt fast wie Magie – wie schafft es eine JavaScript-Bibliothek, die Benutzeroberfläche so effizient zu aktualisieren?
Der Algorithmus heißt Reconciliation, und er ist ein guter Ausgangspunkt.
Reconciliation – Ein Algorithmus, der React möglich gemacht hat
Zwei DOM-Bäume von Grund auf zu vergleichen und die minimale Anzahl an notwendigen Änderungen zu berechnen, ist ein Problem, das in der Regel etwa O(n³) Zeit in Anspruch nimmt, um gelöst zu werden.
Aber nehmen wir an, der Abgleichsvorgang stützt sich auf einige vernünftige Annahmen, und man gibt ihm ein paar Hinweise darüber, wo sich die Änderungen wahrscheinlich befinden?
Schon erledigt! Die Komplexität sinkt auf etwa O(n).
Das bedeutet, dass ein Ansatz, der naiv etwa 31 Jahre zum Abschluss bräuchte, auf etwas unter 16 Minuten verkürzt wird – rein durch die Nutzung von Annahmen und ein wenig Anleitung seitens des Entwicklers.
Einen Moment … Hinweise? Was sind das genau für Hinweise, und wie liefern wir sie?
Kein Grund zur Sorge. Es ist einfacher, als es klingt, und wir werden das bald genauer erläutern.
Fürs Erste lassen wir den Abgleich im Hintergrund arbeiten und wenden uns der Frage zu, die für uns Entwickler wirklich wichtig ist.
Aber warum überhaupt React verwenden?
Warum React?
Egal, was dir auch immer gesagt wird – es erfordert tatsächlich eine mentale Anpassung, wenn man von einfachen HTML-, CSS- und JavaScript-Dateien zu Konzepten wie Komponenten, Hooks, Props und State übergeht.
Neben Tools wie FastAPI oder Go kann React tatsächlich den Eindruck erwecken, einen steileren Lernweg zu verlangen.
Aber hier ist der Punkt: diese Schwierigkeiten liegen am Anfang.
Sobald man den „Vorhang“ hinter React lüftet, stellt man fest, dass viele dieser auf den ersten Blick beängstigenden Konzepte auf überraschend einfachen Grundlagen beruhen.
Man muss nicht gleich die gesamte Anwendung im Kopf behalten.
Stattdessen kann man sich auf eine einzelne Komponente konzentrieren – welche Daten sie erhält, was sie verfolgen muss und wie ihre angezeigte Ausgabe aktualisiert werden sollte.
Genau diese getrennte Denkweise macht es möglich, komplexe Schnittstellen zu entwickeln und zu warten. Letztendlich ist das das Ergebnis, das Entwickler wirklich interessiert.
Code schreiben, der leichter entwickelt, verstanden, geändert und wartungsfreundlich ist. Das Ziel hier ist, Ihnen dabei zu helfen.
Die vier Säulen von React :—
Komponenten
Im Kern ist eine Komponente eine JavaScript-Funktion, die ein einzelnes Objekt entgegennimmt und etwas UI zurückgibt, das in JSX geschrieben ist.
JSX ist eine Syntaxerweiterung für JavaScript, die es ermöglicht, markupsähnliche Strukturen direkt in den JavaScript-Code einzubetten. Sie können Elemente mit herkömmlichem CSS oder einer Utility-Bibliothek wie Tailwind CSS gestalten.
Jede ausreichend komplexe React-Anwendung ist letztendlich nur ein Netzwerk von Komponenten, die Daten miteinander austauschen.
Nehmen wir an, Sie haben noch nie mit React gearbeitet – Sie können trotzdem verstehen, was eine Komponente tut.
const data = {
question: "What is React?",
answer: "A JavaScript library for building user interfaces"
};
function FlashCard(data) {
return (
<div className="border rounded-lg bg-gray-50 p-4">
<h2>{data.question}</h2>
<p>{data.answer}</p>
</div>
);
}
Wenn man ein paar syntaxspezifische Besonderheiten von React ignoriert, ist das im Grunde die ganze Idee hinter React.
Nicht schlecht, oder?
Im Kern geben wir Daten an eine Funktion weiter und erhalten im Gegenzug ein UI-Element zurück.
Als Entwickler besteht Ihre eigentliche Aufgabe darin, saubere Komponenten zu entwerfen und dabei drei Fragen im Hinterkopf zu behalten:
- Welche Informationen nimmt sie auf?
- Welche Informationen behält sie bei?
- Wie sollte ihr Erscheinungsbild aktualisiert werden?
Was könnte schon kompliziert daran sein, ein paar Parameter und einige Variablen zu verwenden? Können wir nicht einfach die Daten übergeben, die wir benötigen, und alles, was wir wollen, in lokalen Variablen speichern?
Irgendwie schon – aber nicht ganz.
Und was meinen wir mit UI-Update? Stellen Sie sich zwei Verkäufer vor, die versuchen, denselben Auto zu verkaufen, wobei der Manager nur einem von ihnen von einer Preisänderung erzählt.
Was passiert mit dem anderen Verkäufer? Er gibt weiterhin den alten Preis an, ohne etwas zu ahnen.
Stellen Sie sich nun vor, der Manager postet die Aktualisierung in einer Gruppenchat-Gruppe, an der alle teilnehmen. Ändert sich der Preis einmal, sieht das ganze Team es sofort.
Das ist im Grunde das Problem, das React lösen soll.
Sobald sich Informationen ändern, die das Auf dem Bildschirm Anzeigbare beeinflussen, benötigt alles, was davon abhängt – in unserer Analogie die Personen, in React die Komponenten – eine zuverlässige Methode, um diese Änderung festzustellen und darauf zu reagieren. Genau diesen Mechanismus bietet React.
Props
Sind function User(name, age, city) und function User(name, city) austauschbar? Wie sieht es mit function User(city, name) aus?
Eine typische Funktion, die sich auf positionale Parameter verlässt, kann nur eine bestimmte Anzahl von Argumenten in einer festgelegten Reihenfolge akzeptieren – und denken Sie daran: Eine Komponente ist im Grunde nur eine Funktion.
Stellen Sie sich nun vor, Sie hätten eine Komponente entwickelt, die den Namen und das Alter eines Benutzers anzeigt, und diese wird derzeit an 67 verschiedenen Stellen in Ihrer Codebasis verwendet. Dann bittet Ihr Vorgesetzter Sie, auch die Stadt des Benutzers anzuzeigen. Jede einzelne Verwendung zu aktualisieren, würde stundenlange Arbeit erfordern.
Aber was wäre, wenn die Funktion so konzipiert wäre, dass alte Aufrufe unverändert weiterhin funktionieren, während neue Aufrufe optional zusätzliche Daten übergeben könnten?
Das Werkzeug, das Sie hier benötigen, ist ein einzelnes Objekt, das eine Parameterliste ersetzt. React bezeichnet dies als Props.
Props sind einfach die Methode, mit der React alle an eine Komponente übergebenen Daten in ein einziges Objekt zusammenfasst.
/** So a component like this one **/
function User({ name = "Guest", age = 18, city = "Unknown" }) {
return (
<div>
<h2>{name}</h2>
<p>{age} years old</p>
<p>{city}</p>
</div>
);
}
/** Can be used in ways like **/
<User /> /** Guest, 18, Unknown **/
<User name="Pritam" /> /** Pritam, 18, Unknown **/
<User name="Rahul" age={21} /> /** Rahul, 21, Unknown **/
<User name="Priya" age={22} city="Delhi" /> /** Priya, 22, Delhi **/
Zustände
Sollte der Verkaufsleiter den neuen Preis für sich behalten? Nur einem Verkäufer mitteilen? Beiden? Oder ihn allen im Autohaus bekannt geben, einschließlich der Sicherheitskräfte und des Reinigungspersonals?
Stellen Sie sich eine Aufbewahrungsbox vor, die Sie mit Ihren Sachen füllen. Können Sie bereits beim Hinsehen auf die Box von außen erkennen, ob sich etwas darin verändert hat? Was passiert, wenn die Box auf einen anderen Regalplatz gebracht wird? Zumindest würden Sie bemerken, dass sich ihre Position verschoben hat, und daraus schließen, dass etwas geschehen ist.
Bilden Sie sich nun vor, diese Box sei das Mechanismus, den React verwendet, um die von Ihnen definierten Werte zu speichern.
Das bedeutet, es muss eine Möglichkeit geben, aktualisierte Werte an alle weiterzuleiten, die von ihnen abhängen, sowie eine Möglichkeit dafür, dass diese Abhängigen wissen, wann ihr Wert sich geändert hat.
const [count, setCount] = useState(initialCount);
Sind Sie bereits auf diesen Aufruf von useState gestoßen?
Er übergibt der Komponente zwei Dinge:
- count – den aktuellen State-Wert.
- setCount – eine Funktion, die Sie aufrufen, um den State-Wert aktualisieren zu lassen.
Was ist also falsch daran, einfach value = 5 zu schreiben?
React hat keine Möglichkeit, herauszufinden, dass sich etwas innerhalb der Box geändert hat!
Anders ausgedrückt: Man benötigt einen Mechanismus, um React mitzuteilen: „Hey, ich habe diesen Wert aktualisiert – du solltest vielleicht die Benutzeroberfläche erneuern.“
Ist das der Hinweis von vorhin? Eher ja und nein.
useState gibt Ihnen die Möglichkeit, einen Wert zu speichern, Updates dafür anzufragen und gleichzeitig React mitzuteilen, dass eine Änderung stattgefunden hat. Aber warum muss React explizit informiert werden?
Weil man sonst genauso unvorsichtig handelt wie jener Manager des Autohändlers.
Erinnern Sie sich an den eigentlichen Fehler? Der Manager änderte den Preis, aktualisierte seine eigene Kopie der Tafel und vergaß, die anderen zu informieren. Sie sind klüger – Sie rufen einfach setCount() auf.
Was passiert also tatsächlich, sobald man setCount() aufruft?
React übernimmt den neuen Zustand, lädt das Komponenten-Element erneut, um herauszufinden, wie die Benutzeroberfläche nun aussehen sollte, und verlässt sich anschließend auf die Abgleichsfunktion, um herauszufinden, was tatsächlich auf dem Bildschirm geändert werden muss.
Man hat React lediglich mitgeteilt, dass eine Sache sich geändert hat. Die Abgleichsfunktion bestimmt, was sich geändert hat und was infolgedessen aktualisiert werden muss.
Hooks
useState() – eine eingebaute Funktion, die es einem Komponenten ermöglicht, einen Zustandswert zu speichern und einen Weg zur Anfrage von Aktualisierungen bereitstellt.
Wenn dieser Zustand aktualisiert wird, können zwei Dinge geschehen:
- Der neue Wert hat keinen Einfluss auf das, was dargestellt wird – React könnte trotzdem neu rendern, aber visuell ändert sich nichts.
Funktionen wie diese, die besondere React-Fähigkeiten bieten, werden Hooks genannt. Es gibt noch einige weitere, die man kennen sollte.
useRef() – nützlich, wenn man einen Container für einen Wert benötigt, der überhaupt nichts mit der Benutzeroberfläche zu tun hat.
const count = useRef(0);
count.current++;
Die Faustregel lautet also: Wenn sich der sich ändernde Wert auch die Benutzeroberfläche verändern lassen soll, verwendet man useState(). Wenn nur der Wert selbst geändert werden muss, ohne dass dies Auswirkungen auf die Darstellung hat, verwendet man useRef().
Es gibt auch einen weiteren häufigen Anwendungsfall – die Verweisung auf echte DOM-Elemente – aber für den Moment reicht dieses Denkmuster aus.
useEffect() – verwendet, wenn etwas nach Abschluss der UI-Berechnung durch React ausgeführt werden muss.
useEffect(() => {
console.log("Runs after every render");
});
useEffect(() => {
console.log("Runs once after the initial render");
}, []);
useEffect(() => {
console.log("After the initial render and whenever count changes");
}, [count]); /** Dependencies go here **/
useContext() – man stelle sich einen Komponentenbaum vor, in dem ein Datensatz wie name mehrere Schichten von Komponenten durchlaufen muss, um schließlich eine tief verankerte UserName-Komponente zu erreichen.
Erweitert man diesen Baum um weitere Datensätze, die durch Komponenten geleitet werden, die sie nicht einmal direkt verwenden, wird das Problem noch gravierender.
Dieses Muster wird als Prop-Drilling bezeichnet.
Reacts Context-API bietet eine Lösung dafür: Sie ermöglicht es, Daten auch in tiefer liegenden Bereichen des Baums verfügbar zu machen, ohne sie manuell durch jede Zwischenkomponente weiterleiten zu müssen.
const ParentContext = createContext(null)
<ParentContext value={money}>
<ChildComponent />
</ ParentContext>
function ChildComponent() {
const money = useContext(ParentContext);
return <p>Money: {money}</p>;
}
Es gibt noch viele weitere Hooks, und es lohnt sich, mit ihnen selbst zu experimentieren.
Bis jetzt ging es um die Art und Weise, wie React Komponenten organisiert und wie Daten zwischen ihnen übertragen werden. Doch eine echte Anwendung muss auch entscheiden, welche Teile dieser Schnittstelle unter welchen URLs angezeigt werden sollen.
React Router
React ermöglicht es Ihnen, Schnittstellen aus Komponenten zu erstellen, die sich selbst aktualisieren können, ohne dass der Browser die gesamte Seite neu laden muss. Doch eine echte Anwendung benötigt in der Regel mehr als eine Seite, was eine neue Frage aufwirft.
Was passiert, wenn Ihre Anwendung mehrere unterschiedliche Ansichten benötigt?
Möglicherweise möchten Sie etwas wie folgt:
/home → Startseite
/dashboard → Dashboard
/profile → Profil
Falls Sie diese mit einfachen HTML-``-Tags verbinden, löst ein Klick darauf eine vollständige Navigation im Browser aus. Die gesamte Seite wird verworfen und die Anwendung startet erneut von vorne unter der neuen Adresse.
Was Sie eigentlich wollen, ist, dass sich die URL aktualisiert, während React leise herausfindet welche Komponenten ausgetauscht werden müssen, ohne alles andere zu vernichten.
Das ist das Problem, das React Router löst.
Betrachten Sie es als eine Schicht, die URLs mit Komponenten verknüpft.
Zum Beispiel:
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
React Router prüft die aktuelle URL und rendernt die damit verbundene Komponente. BrowserRouter nutzt die History API des Browsers, um die Navigation auf der Client-Seite zu steuern, und Routes wählt die Route aus, die am besten zur aktuellen Pfadstruktur passt.
Trotzdem gibt es noch ein weiteres Problem zu bewältigen.
Was passiert, wenn Sie nicht möchten, dass der gesamte Bildschirm neu gerendert wird?
Stellen Sie sich ein solches Layout vor:
┌─────────────────────────────┐
│ Header │
├──────────┬──────────────────┤
│ │ │
│ Menu │ Page content │
│ │ │
├──────────┴──────────────────┤
│ Footer │
└─────────────────────────────┘
Der Wechsel von /home zu /dashboard sollte nicht dazu führen, dass Header, Seitenleiste oder Fußzeile verschwinden und wieder auftauchen. Nur der Hauptinhaltsbereich muss sich ändern.
Dies ist genau das Szenario, für das verschachtelte Routen und <Outlet /> konzipiert sind.
function Layout() {
return (
<>
<Header />
<Menu />
<Outlet />
<Footer />
</>
);
}
Die Routen selbst können anschließend ineinander verschachtelt werden:
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
</Route>
</Routes>
Mit dieser Einrichtung bleibt Layout weiterhin montiert, und React Router ersetzt es durch die passende Kindroute innerhalb von <Outlet />. Laut der eigenen Dokumentation von React Router markiert <Outlet /> den Ort, an dem die passende Kindroute gerendert wird.
Der index-Pfad entspricht <Home /> dem Pfad /, wodurch er die Standardansicht darstellt, die innerhalb des Outlets angezeigt wird, wenn kein spezifischerer Pfad aktiv ist.
Daher geht es beim Wechsel von /home zu /dashboard nicht wirklich darum, die gesamte Seite zu ersetzen. Es ist eher so zu verstehen:
">Lassen Sie diesen Teil der Benutzeroberfläche unverändert und ersetzen Sie nur diesen Abschnitt durch den Komponenten, der zum neuen Pfad gehört."
Was kommt als Nächstes?
Sobald Sie ein solides Verständnis dafür haben, welche Probleme React lösen soll und welche Mechanismen es dazu verwendet, lohnt es sich, Zeit damit zu verbringen, echte Codebasen durchzulesen, um zu sehen, wie erfahrene Teams ihre Anwendungen strukturieren.
Suchen Sie nach einem Repository, das zeigt, wie eine Produktions-React-Anwendung in großem Maßstab organisiert und architektonisch gestaltet werden kann.
Anstatt zu versuchen, den gesamten Codebase auf einmal zu durchdringen, wählen Sie eine einzige Funktion und verfolgen Sie sie durch die verschiedenen Schichten der Anwendung. Achten Sie darauf, wie Komponenten gruppiert sind, woher die Daten stammen, wie der Zustand verarbeitet wird und wie verschiedene Teile der Anwendung miteinander kommunizieren.
Auch lohnt es sich, ein Beispiel zu finden, das zeigt, wie React mit Redux in einer funktionsfähigen Anwendung kombiniert werden kann.
Falls das Projekt, das Sie finden, älter ist, sollten Sie seine Muster nicht als aktuellen Standard für React betrachten. Nutzen Sie es stattdessen, um zu untersuchen, wie eine große Anwendung in Teile aufgeteilt werden kann und wie Redux in diese Struktur passt.
Sobald Sie sich mit der typischen Dateistruktur einer React-Anwendung vertraut gemacht haben und in der Lage sind, eigene einfache Komponenten zu erstellen, ist der nächste Schritt, zu lernen, wie Anwendungen in großem Maßstab entwickelt werden können.
Das Skalieren einer React-Anwendung bringt eigene Herausforderungen mit sich, darunter:
- SSR und Server Components – wie sich das Verhalten ändert, wenn Teile der Anwendung auf dem Server statt vollständig im Browser ausgeführt werden.
- Zustandsverwaltung – was zu tun ist, wenn der Zustand der Anwendung zu groß wird oder zu weit verbreitet ist, um von lokalem Zustand und Context komfortabel verwaltet zu werden. Redux ist eine von mehreren Optionen.
- Datenabruf und Caching – wie Produktivanwendungen Ladeindikatoren, Fehlerbehandlung, Caching sowie die Synchronisierung von Client-Daten mit dem Server verwalten.
- Leistung – erkennen, wann das Rendering tatsächlich zu einem Engpass wird, und entsprechend optimieren, anstatt bereits im Voraus alles zu optimieren.
Man muss nicht jedes dieser Themen beherrschen, bevor man mit dem Entwickeln beginnt.
Ein praktischerer Ansatz ist es, mit dem Entwickeln zu beginnen, auf ein spezifisches Problem zu stoßen und anschließend das jeweilige Konzept oder Tool zu erlernen, das dieses Problem löst.
Letztendlich bestand der Zweck des Lernens von React nie darin, seine API-Oberfläche auswendig zu lernen. Es ging vielmehr darum, zu verstehen, warum diese APIs existieren und wie man über die Probleme nachdenkt, für die sie entwickelt wurden.
Zusätzliche Literatur
- Verständnis von React Custom Hooks: Wiederverwendung von Logik ohne gemeinsamen Zustand — Erfahren Sie, was React Custom Hooks sind, wie sie zustandsbasierte Logik zwischen Komponenten extrahieren und teilen sowie welche häufigen Fehler bei ihrer Erstellung vermieden werden sollten.
- React-Designmuster: Vom klassischen OOP bis zu modernen Hooks — Erklärt, wie klassische Softwaremuster wie Singleton, Factory und Observer in React Anwendung finden, sowie reaktions-spezifische Muster wie HOCs, Hooks und Kombinationskomponenten.