Im Vergleich zu anderen Frontend-Frameworks wie Angular oder Vue bietet React relativ viele Freiheiten und macht wenig Vorgaben hinsichtlich des Aufbaus und der Architektur einer Applikation. Für fortgeschrittene Entwickler stellt das weniger ein Problem dar. Für Einsteiger bedeutet es jedoch eine vergleichsweise hohe Einstiegshürde. Aus diesem Grund haben sich verschiedene Muster etabliert, die die Strukturierung einer Applikation erleichtern.

Dieses Kapitel befasst sich mit einigen der wichtigsten Entwurfsmustern aus dem React-Universum. Den Anfang macht das fundamentalste Merkmal einer React-Applikation: die Komponenten-Architektur.

Weitere Experten-Beiträge mit Tipps für die Arbeit mit React in unserem E-Book. E-Book React – Jetzt kostenlos downloaden!

Der komponentenorientierte Ansatz von React

Allen React-Applikationen liegt eine komponentenorientierte Architektur zugrunde. Diese beeinflusst den grundlegenden Aufbau einer Applikation entscheidend. Eine Applikation, die diesem Architekturansatz folgt, ist wie ein Baum aufgebaut. Eine Wurzelkomponente bildet die Basis und verzweigt sich in eine Hierarchie von Kind-Komponenten.

Bei der Implementierung besteht der erste Arbeitsschritt daraus, die gewünschte Oberfläche in eine Komponentenhierarchie zu zerlegen. Eine solche Komponentenhierarchie ist in den seltensten Fällen frei von Zuständen. Eine Tabellenzeile kann beispielsweise hervorgehoben, ein Datensatz inaktiv oder ein Button versteckt sein. Diese Zustände werden in Form eines Komponenten-States innerhalb der Hierarchie gespeichert. Die Platzierung des States ist abhängig davon, welche Komponenten die Informationen benötigen. Im einfachsten Fall könnte der gesamte State der Applikation in der Wurzelkomponente abgelegt werden. Dies hätte jedoch den Nachteil, dass die Informationen an die Kind-Komponente weitergereicht werden müssen, die sie zur Anzeige benötigt.

Bei einer Tiefe von zwei oder drei Komponenten-Ebenen ist das Durchreichen von State-Informationen noch praktikabel. Wird der Baum jedoch tiefer, stößt man schnell an die Grenzen der Übersichtlichkeit und Wartbarkeit. Aus diesem Grund sollten Sie darauf achten, den State nicht unnötig weit von der Stelle zu platzieren, an der er benötigt wird. Die Verteilung der State-Informationen erfolgt über die sogenannten Props. Das sind im weitesten Sinne so etwas wie die Parameter einer Komponente. Sie können das Verhalten einer Komponente von außen beeinflussen und tragen damit zu einer besseren Wiederverwendbarkeit einer Komponente bei. Props und State sind zwei fundamentale Elemente in einer komponentenbasierten Architektur. Aus diesem Grund finden Sie diese beiden Konstrukte nicht nur in React, sondern auch in den anderen wichtigen Frontend-Frameworks.

Sowohl Angular als auch Vue verfolgen ebenfalls einen komponentenbasierten Ansatz beim Aufbau eines Web-Frontends. Jedoch geht React hier einen bemerkenswerten Weg: Das Erzeugen einer Komponente ist sehr einfach und leichtgewichtig. Die einzigen Anforderungen an eine solche React-Komponente sind, dass es sich um eine Funktion handeln muss und dass diese eine JSX-Struktur zurückgeben muss. Zwar ist es auch möglich, Komponenten auf Basis von JavaScript-Klassen zu formulieren, in modernen React-Applikationen ist diese Art der Komponenten jedoch kaum noch anzutreffen.

Da es React Ihnen als Entwickler so einfach macht, neue Komponenten zu erzeugen, sind React-Komponenten tendenziell kleiner als in den anderen Frameworks. Sie sollten Ihre Komponenten nach Möglichkeit auch mit einem Maximum an Wiederverwendbarkeit designen. Damit reduzieren Sie zum einen die Anzahl der Komponenten in einer Applikation, was eine bessere Übersicht schafft, zum anderen können Sie solche wiederverwendbaren Komponenten auch in anderen Applikationen nutzen. Mit der Zeit schaffen Sie sich so eine Bibliothek aus häufig verwendeten Komponenten, die Ihnen gerade bei Standardaufgaben sehr viel Arbeit abnehmen.

Die Grundlage einer wiederverwendbaren Komponente ist das bereits erwähnte Konzept der Props. Mit diesen Parametern der Komponente können Sie beispielsweise einen Datensatz an die Komponente übergeben, den Sie rendern möchten, oder Konfigurationsinformationen, die das Aussehen der Komponente bestimmen. Die Props einer Komponente können von beliebigen Datentypen sein. Es ist sowohl erlaubt, einfache Datentypen als auch Objekte und Arrays an eine Komponente zu übergeben. Auch Funktionen kommen zum Einsatz und zwar vor allem, wenn eine Komponente mit Ihrer Elternkomponente kommunizieren muss.

Abbildung - Komponentenhierarchie

Komponentenhierarchie

Die Props einer Komponente haben noch einen weiteren Zweck, als den reinen Datenfluss in einer Komponentenhierarchie abzubilden. Eine Änderung der Props führt automatisch dazu, dass die Komponente neu gerendert wird.

 

TypeScript

Mit den Props wird auch gleich eine potenzielle Schwäche der Kombination aus nativem JavaScript und React deutlich: Der Entwickler einer Komponente kann keine Typen für die Props festlegen. Je mehr Personen an einer Applikation arbeiten, desto deutlicher wird das Problem, da kein direkter Austausch zwischen den einzelnen Entwicklern stattfindet und es tendenziell auch zu viele Komponenten gibt, um den Überblick zu behalten. Dieses Problem kann einfach mit der prop-types-Bibliothek gelöst werden.

Eine bessere, weil umfassendere Lösung, bietet der Einsatz eines Typsystems, und hier ist die am häufigsten anzutreffende Variante TypeScript. Mittlerweile unterstützt Create React App die Initialisierung einer React-Applikation mit TypeScript, so dass bereits zu Beginn mit TypeScript gearbeitet werden kann und auch schon eine passende Konfiguration aller Entwicklungs- und Build-Werkzeuge vorliegt. Nachfolgend sehen Sie ein Beispiel einer einfachen Komponente, die TypeScript-Features wie beispielsweise die Typisierung der Props nutzt.

Komposition

React hat seit dem Release der Version 16 eine rasante Entwicklung durchgemacht. Nachdem das Entwicklerteam den Fiber Reconciler vorgestellt hat, wurden im Zuge einer Reihe von Minor-Releases zahlreiche neue Features eingeführt, die die Entwicklung von Applikationen entscheidend beeinflussen.

Das wohl wichtigste Feature war die Einführung der Hook-API. Mit ihr hat das Entwicklerteam von React die Funktionskomponenten auf Augenhöhe mit den Klassenkomponenten gebracht. Dabei gibt es einige Vorteile, die für den Einsatz von Funktionskomponenten sprechen: Sie kommen ohne das this-Schlüsselwort aus, außerdem können State und Lifecycle-Methoden aufgeteilt werden. Innerhalb einer Klassenkomponente können Sie über this auf die aktuelle Instanz der Klasse zugreifen. Innerhalb von Callback-Funktionen, wie sie beispielsweise bei Eventhandlern zum Einsatz kommen, verweist this jedoch nicht mehr auf das Objekt.

Dieser Umstand ist eine häufige Fehlerquelle und kann beispielsweise verhindert werden, wenn Sie die Methoden in den Objektkontext binden oder statt regulärer Methoden Eigenschaften mit Arrow-Funktionen als Wert verwenden. Ein weiterer Nachteil bei den Klassenkomponenten ist, dass es keine Möglichkeit gibt, den State und die Lifecycle-Methoden einer Klassenkomponente zu unterteilen. Mit der Kombination aus Funktionskomponenten und der Hook-API wird es möglich, mehrere State-Objekte innerhalb einer Komponente vorzuhalten. Damit können Sie beispielsweise die Zustände von mehreren GUI-Elementen sauber voneinander trennen. Ähnliches gilt für die Lifecycle. Wo Sie bei einer Klasse nur beispielsweise einmal die componentDidMount-Methode implementieren können, wird es dank des Effect-Hooks möglich, mehrere Funktionen für den gleichen Lifecycle-Abschnitt zu implementieren.

Der Lifecycle einer Komponente

Der Lifecycle einer Komponente (erstellt von Dan Abramov: https://twitter.com/dan_abramov/status/981712092611989509/)

Die Einführung der Hook-API hat insgesamt dazu geführt, dass die Strukturen einer React-Applikation besser aufgeteilt werden können und sich bestimmte Codeblöcke nur noch um eine Sache kümmern. Die Custom Hooks, eine Kombination aus den Builtin-Hooks von React und eigener Implementierung, erlauben es, die Applikationslogik von der Komponente zu trennen. Dies dient zum einen dazu, den Code einer Komponente kleiner zu halten, und zum anderen – und das ist der eigentliche Verwendungszweck von Custom Hooks – können Sie damit wiederverwendbare Hooks erzeugen. So können Sie beispielsweise einen konfigurierbaren Custom Hook implementieren, der beim Laden der Komponente Daten vom Server lädt und im lokalen State festhält.

Hier sehen Sie ein Beispiel für einen solchen Custom Hook:

Der allgemeinen Konvention folgend beginnt der Name der Hook-Funktion mit dem Präfix use. Wann immer Sie also in React eine Funktion, die mit use beginnt, nutzen, ist dies ein klarer Hinweise darauf, dass es sich dabei um eine Hook-Funktion handelt. Wie jeden Custom Hook können Sie auch die Funktion aus dem Beispiel in einer beliebigen Funktionskomponente einbinden und müssen beim Aufruf lediglich die URL des serverseitigen Endpunkts übergeben, von dem die Daten bezogen werden sollen. Da der Custom Hook als TypeScript Generic implementiert ist, haben Sie auch die vollständige Unterstützung des TypeScript-Compilers und Ihrer Entwicklungsumgebung.

Dieser Hook lässt sich noch erweitern, so dass er beispielsweise Funktionen zum Modifizieren oder Löschen einzelner Datensätze zur Verfügung stellt. Auch dies sind Standardaufgaben, die sich bis auf die Datenstrukturen, auf denen sie arbeiten, nicht sonderlich unterscheiden.

Die meisten Bibliotheken im React-Ökosystem haben in der Zwischenzeit auf Hooks umgestellt oder bieten zumindest eine Hook-Implementierung an. Ein recht prominentes Beispiel ist hier Redux. Die zentrale State-Management-Bibliothek fühlt sich dank der Hook-API nicht mehr wie ein Fremdkörper an, sondern gliedert sich nahtlos in das Frontend ein.

Patterns aus der alten Welt neu interpretiert

Zwei Patterns, die React über eine lange Zeit begleitet haben, sind Render Props und Higher Order Components, oder kurz HoC. Beide Muster dienen dazu, eine Komponente mit zusätzlichen Informationen anzureichern. In der Implementierung von einfachen Applikationen kommen sowohl Render Props als auch HoCs eher selten vor. Dafür waren sie vor dem Aufkommen der Hook API das Quasi-Standardmittel von Bibliotheken, um ihre Features zur Verfügung zu stellen. Bei den Render Props wird eine Komponente implementiert, die entweder eine render-Prop aufweist, daher der Name, oder die über die children-Prop auf ihr Kindelement zugreift, das in diesem Fall eine Funktion ist. Die außenliegende Komponente implementiert wiederverwendbare Logik und übergibt die Informationen beziehungsweise Funktionen an die render-Funktion.

Bei einer HoC handelt es sich um eine Funktion, die eine Komponente als Argument erhält und eine Komponente zurückgibt. Dieses Muster verfolgt den gleichen Zweck wie schon die Render Props mit dem Unterschied, dass hier etwas deutlicher wird, dass durch die HoC eine Eingabekomponente angereichert wird. Typischerweise beginnen HoC-Funktionen mit dem Präfix with. Ein prominentes Beispiel einer solchen Implementierung ist der React Router. Die withRouter-Funktion ist genau eine solche HoC-Funktion. Übergeben Sie eine Ihrer Komponenten an diese Funktion, wird sie um Routing-Informationen angereichert, und Sie können über die Props Ihrer Komponente beispielsweise auf den aktuellen Pfad und Routing-Parameter zugreifen.

Der Vorteil von Render Props und HoC ist, dass beide im Gegensatz zu Hooks mit Klassenkomponenten funktionieren und auch mit Funktionskomponenten verwendet werden können. Beide Muster werden jedoch zunehmend von Hook-Implementierungen verdrängt. Am Beispiel des React Routers und der useParams-Funktion sehen Sie, dass Sie statt über die HoC-Funktion über eine Hook-Funktion auf die Routing-Parameter zugreifen können.

Anordnung und Aufbau einer Applikation

Wie schon erwähnt, macht React kaum Vorgaben. Das gilt sowohl für die Form des Quellcodes als auch die Platzierung des Quellcodes im Dateisystem. Theoretisch können Sie sämtliche Komponenten, Hooks und alle weiteren Strukturen Ihrer Applikation in eine einzige Datei ablegen. Beim Build der Applikation optimieren Werkzeuge wie Webpack den Quellcode und fassen die Komponenten in eine, beziehungsweise wenige Dateien.

Grundsätzlich sollten Sie darauf achten, dass der Quellcode Ihrer Applikation auf Lesbarkeit hin optimiert ist. Die Performance-Vorteile, die Sie durch eine übermäßige Optimierung herausholen, gehen sehr auf Kosten der Lesbarkeit. Das bedeutet, dass es teurer wird, Ihre Applikation zu erweitern oder Fehler zu beheben. Deshalb hat sich „eine Komponente pro Datei“ als Best Practice etabliert.

Verfügt Ihre Komponente über Props, können Sie das Props-Interface innerhalb der Komponenten-Datei definieren, da das Interface in der Regel an keiner weiteren Stelle benötigt wird. Aus diesem Grund wird dieses Interface auch nicht exportiert. Beim Export der Komponente können Sie zwischen default und named Export wählen. Wichtig ist an dieser Stelle nur, dass Sie innerhalb Ihrer Applikation konsistent bleiben und überall die gleiche Art verwenden.

Das Styling Ihrer Komponenten können Sie ebenfalls in der Komponenten-Datei selbst vornehmen. Je nachdem, wie viel komponentenspezifisches Styling in Ihrer Applikation notwendig ist, kann durchaus eine größere Menge an Styling-Code entstehen. Dies macht die Komponenten-Datei unübersichtlich, was für die Auslagerung des Stylings in eine separate Datei spricht.

Wählen Sie diesen Weg, sollten Sie darauf achten, dass die Styles möglichst nah bei der Komponenten-Datei abgelegt werden. Für die Unit-Tests Ihrer Komponenten gilt übrigens das gleiche: Legen Sie die Testdatei bei Ihrer Komponente ab. Damit haben Sie alle Dateien, die die Komponente betreffen, in einem Verzeichnis und müssen nicht suchen, beziehungsweise das Verzeichnis wechseln.

Für die Benennung der Dateien sollten Sie ebenfalls ein einheitliches Schema wählen. Häufig kommt entweder CamelCase oder Kebab-Case als Schreibweise zum Einsatz, wobei Zweiteres Probleme mit case-sensitiven Dateisystemen vermeidet. Die Komponentendateien enden mit .tsx. Die Styling-Dateien heißen in der Regel wie die Komponenten-Dateien und unterscheiden sich lediglich durch die Dateiendung. Verwenden Sie beispielsweise SCSS, lautet die Endung .scss. Die Testdatei heißt ebenfalls wie die Komponentendatei und endet auf .spec.ts.

Für die Aufteilung der Dateien in Verzeichnisse gilt, dass Sie bei umfangreicheren Applikationen mit einer fachlichen Trennung starten sollten. Haben Sie beispielsweise einen Bereich in Ihrer Applikation, der sich mit Benutzermanagement beschäftigt, erzeugen Sie ein Verzeichnis „user“. Hier finden sich alle Dateien, die mit diesem Bereich zu tun haben. Als Daumenregel gilt, dass Sie ein Verzeichnis in Unterverzeichnisse unterteilen sollten, sobald in dem Verzeichnis mehr als sieben bis zehn Dateien liegen, da ab dieser Zahl die Übersicht beginnt verloren zu gehen.

React Patterns – Fazit

React ist eine Frontend-Bibliothek, die Ihnen als Entwickler sehr viele Freiheiten bietet. Aus diesem Grund ist es wichtig, bei der Implementierung einer Applikation noch mehr auf die Einhaltung von Konventionen zu achten. Ein wichtiger Baustein sind dabei die Strukturen, mit denen bestimmte Arten von Problemen gelöst werden. Der konsistente Einsatz solcher Lösungsmuster verbessert die Wartbarkeit einer Applikation, da sich die Entwickler einfacher und schneller zurechtfinden. Mit der Einführung der Hook API in React haben sich viele dieser Muster geändert. Generell geht der Trend in Richtung Komposition von Applikationslogik und kleinerer wiederverwendbarer Einheiten.

 

Sebastian Springer
Letzte Artikel von Sebastian Springer (Alle anzeigen)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Die von Ihnen hier erhobenen Daten werden von der Host Europe GmbH zur Veröffentlichung Ihres Beitrags in diesem Blog verarbeitet. Weitere Informationen entnehmen Sie bitte folgendem Link: www.hosteurope.de/AGB/Datenschutzerklaerung/