Frontend
React.js
Autor: Patrick Vogt
Stand: React-Version 16.4.1
Präsentation und SW-Demo: Hier
- Grundlagen
- Konzepte, Technologien und Architekturen im Frontend
- React Technologien und Konzepte
- Literaturverzeichnis
Die folgende Mind-Map visualisiert den Zusammenhang der Inhalte:
Grundlagen
Als grundlegende Einführung soll im Folgenden zunächst ein Überblick über einige Basics bezüglich der Entwicklung von modernen Frontend-Applikationen geschaffen werden. Hierzu werden die Begriffe "Single Page Application" und "Progressive Web App" vorgestellt. Im Anschluss des Kapitels werden Konzepte, Technologien und Architekturen im Frontend vorgestellt sowie spezielle React Technologien und Konzepte erläutert. React ist eine komponentenbasierte JavaScript Bibliothek zur Erstellung von User-Interfaces und wurde 2013 von Facebook veröffentlicht. Sie ist plattformunabhängig und unter der MIT-Lizenz kostenfrei verfügbar.
Single Page Applications
Webseiten bzw. Webanwendungen können auf unterschiedliche Arten umgesetzt werden. Eine Möglichkeit der Unterscheidung besteht darin, "traditionelle" Webseiten und Single Page Applications (SPAs) zu differenzieren. Eine Gegenüberstellung des traditionellen Lifecycles einer Webseite im Vergleich zu dem Lifycycle einer Single Page Application wird in der folgenden Abbildung dargestellt:
Abbildung entnommen aus [WASS13]
In der Abbildung wird der grundsätzliche Unterschied beider Typen deutlich.
Traditionell sendet der Client (Browser) eine initiale Anfrage an den Server, der wiederum mit einer HTML-Datei antwortet. Die Datei wird dann im Browser angezeigt. Wechselt der Benutzer die Seite (z.B. durch das Klicken eines Links), wird eine weitere Anfrage an den Server geschickt. Dieser den HTML-Inhalt der neuen Webseite, woraufhin beim Client ein erneutes Laden durchgeführt wird.
Bei Single Page Applications wird initial ebenfalls eine "normale" Anfrage an den Server versendet und von diesem ein (meist sehr kleiner) HTML-Inhalt zurückgeliefert. Von hier an unterscheidet sich der Lifecycle jedoch vom traditionellen Verfahren. Es werden AJAX- (Asynchronous JavaScript and XML) statt HTML-Anfragen an den Server gesendet, woraufhin dieser typischerweise mit JSON Strings antwortet. Hierbei wird kein erneutes Laden der Webseite durchgeführt.
Da die Webseiten nicht neu geladen werden müssen, bieten sie den Benutzern i.d.R. ein "flüssigeres" Erlebnis. Zudem wird durch das Separieren der Daten (JSON) und der Anzeige (HTML) das Entwerfen von gut strukturierten Webanwendungen vereinfacht. Idealerweise kann das HTML-Markup ohne das Ändern der Logik angepasst werden.
Bei einer reinen Single Page Application wird jegliche UI-Interaktion beim Client durch JavaScript und CSS durchgeführt, sodass der Server nach der anfänglichen Anfrage nur noch als eine Art "Service-Schicht" fungiert. Somit kann sowohl der Server als auch der Client einfach ausgetauscht werden, sofern die neuen Module die entsprechende Schnittstelle bedienen [WASS13].
Progressive Web Apps
Eine Progressive Web App (PWA) ist eine mobile Webseite, die den Nutzern als herkömmliche (native) Anwendung erscheint. Die Webseiten zeichnen sich durch folgende Merkmale aus:
Reliable
Wenn die Anwendungen vom Home-Screen gestartet werden, laden sie (unabhängig vom Netzwerkstatus) quasi sofort.
Fast
Allgemein schnelle Ladezeiten
Engaging
Sie sind ohne App Store installierbar, erscheinen (wie native Apps) als Vollbild-Anwendung und können Push Notifications versenden.
Zu den Vorteile von PWAs zählen unter anderem [GOOG18a]:
- Sie können als Webanwendung auf dem Home Screen der Nutzer verfügbar sein
- Schnelleres initiales Laden (weniger Datenverkehr)
- Web Push Notifications können dabei helfen, dass Nutzer mehr Zeit auf einer Webseite verbringen (siehe Fallstudie)
Zur Unterstützung beim Testen von Progressive Web Apps, stellt Google eine Checkliste und ein Tool zur Verfügung, anhand derer eine Anwendung dahingehen geprüft werden kann, ob sie Googles Kriterien an eine PWA erfüllt [GOOG18b].
Zu diesen Kriterien gehören beispielsweise:
- Die Webseite wird über HTTPS geliefert
- Die Webseiten ist responsive (für Tablets und Mobilgeräte)
- Alle URLs laden offline
- Die Seitenübergänge sind flüssig
Eine Architekturart, die das Erstellen von PWAs erleichtern soll, wird im Kapitel App Shell Model vorgestellt.
Konzepte, Technologien und Architekturen im Frontend
Dieses Kapitel soll einen Überblick über einige Konzepte, Technologien und Architekturen in Bezug auf die Entwicklung von Frontend-Anwendungen vermitteln. Hierzu werden zunächst die App Shell Architektur für PWAs sowie die Motivation für durchdachte State Management Ansätze in Frontend-Anwendungen dargelegt. Anschließend wir das unidirektionale Frontend-Architekturmuster Flux und die Themengebiete JSX, Code-Splitting sowie serverseitiges Rendern vorgestellt.
App Shell Model
App Shell Model (oder Application Shell Model) ist eine Architektur, die das Erstellen von zuverlässigen und sofort ladenden Progressive Web Apps ermöglicht. Die folgende Abbildung zeigt den grundsätzlichen App-Aufbau.
Abbildung entnommen aus [OSMA18]
Die "Applpication Shell" besteht aus wenigen HTML, CSS und JavaScript Bestandteilen, die die grobe App-Struktur definieren. Dieses Grundgerüst kann im Offline-Cache gespeichert werden, wodurch ein erneutes Laden der Anwendung schnell und ohne eine Netzwerkverbindung durchgeführt werden kann. Der benötigte Seiteninhalt ("Content") wird anschließend aus dem Netzwerk geladen und anschließend in die Shell eingebettet.
Single Page Applications können dieses Verfahren z.B. mithilfe von sogenannten "Service Workern" durchführen. Hierbei handelt es sich um Skripte, die vom Browser im Hintergrund ausgeführt werden, um beispielsweise Antworten auf Anfragen offline zu cachen [GRUN18], [OSMA18].
Besondern viel Sinn macht diese Art der Architektur bei der Entwicklung von Webanwendungen, deren Navigationelemente gleich bleiben, sich ihr Inhalt jedoch ändern kann.
State Management
Je komplexer eine Anwendung wird, desto mehr Zustände kann sie einnehmen und desto schwieriger wird dementsprechend das Verwalten ihrer Zustände sowie der entsprechenden Zustandsübergänge. In der Regel verfügen solche Anwendungen über viele Komponenten, die auf bestimmte Zustände reagieren sollen. Die meisten Interaktionen des Benutzers mit der Applikation ändern den Zustand der beteiligten Konmponeten. Angefangen mit dem Betätigen von Schaltflächen, dem Aus- bzw. Abwählen von Checkboxfeldern, bis hin zum Ändern des Wertes einer Combobox. Um diese Komplexität meistern zu können, bedarf es eines strukturierten und durchdachten Konzepts zum Verwalten der Zustände. Dieser Vorgang wird State Management genannt.
Da dieser Themenbereich die meisten Frontend Anwendungen betrifft, verfügen aktuelle Bibliotheken und Frameworks in der Regel über Pakete, die dem Entwickler beim State Management unterstützen.
Das Kapitel State Management in React beschreibt einige Möglichkeiten des State Managements im React-Kontext.
Flux
Flux ist ein Architektur Pattern, das beim Erstellen von User Interfaces verwendet werden kann, um eine bessere Nachvollziehbarkeit der Zustandsänderungen zu erreichen. Es wurde erstmals 2014 von Facebook vorgestellt. Facebook nutzt Flux für Ihre React Projekte, da das Verwenden eines bidirektionalen MVC-Ansatzes - in Verbindung mit der Größe ihrer Projekte - eine hohe Komplexität schafft [FACE14].
Die folgende Abbildung soll diesen Sachverhalt verdeutlichen:
Abbildung entnommen aus [AVRA14]
Im Gegensatz dazu handelt es sich bei Flux um ein unidirektionales Pattern.
Die prinzipielle Architektur von Flux wird in der folgenden Abbildung dargestellt:
Abbildung entnommen aus [TSON18]
Das Pattern besteht aus 4 Bestandteilen [TSON18]:
Action
Ereignisse/Benachrichtigungen, die Informationen/Daten an den Dispatcher weiterleiten. Sie werden von der View oder sonstigen Services (z.B. HTTP Anfragen) ausgelöst.
Typischerweise handelt es sich bei einer Action um ein Objekt, das über zwei Attribute verfügt.
- type: Um welche Action handelt es sich? (Eine Art ID)
- payload: Objekt, das die Nutzdaten enthält
Eine Action, bei der eine Person zu einer Liste hinzugefügt werden soll, könnte folgendermaßen aussehen:
{
type: 'ADD_PERSON',
payload: {
forename: 'Max'
surname: 'Mustermann'
}
}
Um eine bessere Tool-Unterstützung erhalten zu können, kann statt des Strings 'ADD_PERSON' für den Typ (type
) aus dem obigen Beispiel auch z.B. ein Enum-Wert verwendet werden. Wichtig ist hierbei nur, dass der Aktionstyp eindeutig ist und der Wert somit keiner weiteren Aktion zugeordnet wird. In JavaScript kann dies zum Beispiel mithilfe von Symbol()
erreicht werden (siehe Mozilla Beschreibung).
Die Implementierung für eine Liste von Personen könnte wie folgt aussehen:
const PersonActions = Object.freeze({
ADD: Symbol("ADD_PERSON"),
DELETE: Symbol("DELETE_PERSON"),
MODIFY: Symbol("MODIFY_PERSON")
});
Dispatcher
Der Dispatcher ist der hauptsächliche Akteur. Alle eingehenden Ereignisse des Systems (bzw. Actions) werden im Dispatcher gesammelt und von dort aus an die entsprechenden Stores verteilt. Aus diesem Grund können hier sehr gut Logger zu Debugzwecken eingesetzt werden.
Store
Die Stores empfangen die Benachrichtigungen vom Dispatcher und ändern auf Basis der neuen Informationen ggf. ihren Zustand, um Änderungen in der entsprechenden View auszulösen. Im Prinzip handelt es sich bei den Stores somit um das Model im MVC-Pattern.
Aufgrund der einzigartigen Action-Types werden die Daten jeweils nur zu einem Store weitergeleitet, sodass man sich die einzelnen Stores prinzipiell auch als einen großen Store vorstellen kann.
View
Die Views dienen der Anzeige der Daten. Sie abonnieren Stores und können Actions auslösen. Bei React sind die Views beispielsweise die entsprechenden React Anzeige-Komponenten.
JSX
Viele moderne Frontend-Frameworks und -Bibliotheken verwenden JavaScript nicht nur für die reine Logik der Webanwendungen, sondern auch für das Strukturieren der GUI. Dies ermöglicht z.B. ein dynamisches Befüllen der Webseite auf Basis der Werte von JavaScript Variablen. Eine Möglichkeit die syntaktischen Unterschiede zur gewohnten HTML-Schreibweise zu reduzieren, ist der Einsatz von JSX.
JSX erweitert die Programmiersprache JavaScript, indem es eine XML/HTML-artige Struktur zur Programmierung der GUI-Elemente innerhalb des JavaScript Codes erlaubt. Damit aus einem JSX-Code standardmäßiges JavaScript wird, muss der Code übersetzt werden. Dieser Vorgang wird oftmals mithilfe des JavaScript-Compilers "Babel" durchgeführt. Sehr populär ist JSX in Verbindung mit React. Streng genommen ist JSX zwar kein zwingendes Muss bei der Verwendung von React, jedoch ist zu vermuten, dass viele Entwickler die JSX-Version dem kompilierten JavaScript Äquivalent aufgrund der Übersichtlichkeit bevorzugen. Außerdem können so hilfreichere Tool-Unterstützungen (Warnungen, Fehler etc.) angezeigt werden [FACE18a].
Als Beispiel soll folgender JSX-Code dienen:
function render() {
return (
<div>
<h1>Master Informatik</h1>
<h2>Campus Minden</h2>
<h3>Modulname:</h3>
<input type="text" autoFocus name="moduleName"/>
</div>
);
};
Mit HTML vertraute Personen werden vermutlich erahnen können, welche Ausgabe dieser JSX-Code erzeugt. Es wird ein div-Container erstellt, der die Überschriften "Master Informatik" (h1-Element), "Campus Minden" (h2-Element), "Modulname:" (h3-Element) sowie ein Text-Eingabefeld enthält. Das Eingabefeld hat den Namen "moduleName" und erhält beim Erzeugen der Komponente den Eingabefokus.
Dieser Code wird von Babel für React Anwendungen folgendermaßen übersetzt:
function render() {
return React.createElement("div", null,
React.createElement("h1", null, "Master Informatik"),
React.createElement("h2", null, "Campus Minden"),
React.createElement("h3", null, "Modulname:"),
React.createElement("input", {
type: "text",
autoFocus: true,
name: "moduleNameInput"
})
);
};
Das Beispiel lässt sich hier online ansehen und bearbeiten.
Da es sich bei den JSX-Elementen um JavaScript-Objekte handelt, können diese auch Variablen zugewiesen werden:
const header = <h1>Das ist eine Überschrift</h1>;
Des Weiteren lässt sich auch weiterer JavaScript Code direkt als sogenannte "Expression" einfügen, indem dieser in geschweifte Klammern gesetzt wird:
const items = ["book", "pen"];
const itemCounter = <div>Anzahl an Items: {items.length}</div>;
// ===============================================================
// Render-Ausgabe:
// Anzahl an Items: 2
// ===============================================================
Code-Splitting
Bei der Entwicklung von modernen Web Applikationen werden in der Regel eine Vielzahl an einzelnen Paketen verwendet. Tools wie browserify oder webpack bündeln die einzelnen Bestandteile zu einer großen Datei. Hierdurch kann eine Anwendung als Ganzes innerhalb einer Webseite inkludiert werden.
Bei größeren Single Page Applications kann dies zu Problemen führen, da alle Daten geladen werden müssen, obwohl der Benutzer eventuell gar nicht alle Bereiche der Anwendung verwenden wird. Um diesem Problem aus dem Weg zu gehen, kann Code-Splitting eingesetzt werden.
Code-Splitting ermöglicht es, die Bestandteile einer Webanwendung in verschiedene Bereiche aufzuteilen und diese erst bei Bedarf zu laden (quasi lazy loading).
In Verbindung mit beispielsweise webpack kann hierzu die dynamische import()
-Syntax verwendet werden [FACE18g].
Ohne Code-Splitting:
import { SubMenu } from './SubMenu';
SubMenu.open();
Mit Code-Splitting:
// webpack führt hier ein Code-Splitting durch
import("./SubMenu").then(module => {
module.SubMenu.open();
});
Im Folgenden werden zwei weitere Möglichkeiten vorgestellt, wie ein Code-Splitting in Frontendanwendungen durchgeführt werden kann. Auch wenn es sich bei den speziellen Implementierungen um React-Varianten handelt, sind die Prinzipien (komponentenbasiert/routenbasiert) universell einsetzbar.
Aus Gründen der Übersichtlichkeit werden die React-spezifischen Implementierungen bereits hier vorgestellt. Sollte der Quellcode nicht direkt verständlich sein, ist es ggf. sinnvoll diesen Abschnitt zu überspringen und sich die Passagen nach dem Durcharbeiten des React-Kapitels noch einmal anzuschauen.
Komponentenbasiertes Code-Splitting
Bei diesem Ansatz, wir das Code-Splitting auf Basis des "Komponentenzustands" durchgeführt. Der Code wird geladen, sobald die Komponente angefordert wird.
Speziell für React kann hierfür die Bibliothek React Loadable verwendet werden. Sie bietet einen Wrapper für das Code-Splitting und bietet zudem z.B. die Möglichkeit, während des Ladevorganges eine andere Komponente oder einen anderen Text anzuzeigen. Es handelt sich um eine Higher Order Component (HOC), die Promises für das Laden von Komponenten bereitstellt (HOCs werden im Kapitel Higher Order Components (HOCs) vorgestellt) [REAC18].
Ohne Code-Splitting:
import { SomeComponent } from './SomeComponent';
const SomeContainer = () => (
<SomeComponent/>
);
Mit Code-Splitting:
import Loadable from 'react-loadable';
const SomeLoadableComponent = Loadable({
// wird geladen, wenn die Komponente angefordert wird
loader: () => import('./SomeComponent'),
// wird so lange angezeigt, bis die Komponente geladen ist
loading: () => <div>Loading...</div>,
// zeige Ladetext erst nach 0,2 Sekunden an
delay: 200
});
const SomeContainer = () => (
<SomeLoadableComponent/>
);
Routenbasiertes Code-Splitting
Das Code-Splitting kann auch direkt auf Basis der aktuellen Route durchgeführt werden, sodass nur die mit der Route verbundene Komponente/Webseite geladen wird.
Mithilfe von Bibliotheken wie React Router und React Loadable kann dies bei React realisiert werden. Hierbei erhält jede Route eine eigene Komponente, die als Loadable implementiert wird [FACE18g]:
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Loadable from 'react-loadable';
const Loading = () => <div>Loading...</div>;
const Home = Loadable({
loader: () => import('./routes/Home'),
loading: Loading,
});
const About = Loadable({
loader: () => import('./routes/About'),
loading: Loading,
});
const App = () => (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</BrowserRouter>
);
Serverseitiges Rendern
Bei der Erstellung von Webanwendungen kann die Webseite prinzipiell auf verschiedenen Arten gerendert werden.
Clientseitiges Rendern:
Der Browser lädt eine einfache HTML Datei, deren Inhalt durch JavaScript befüllt wird
Serverseitiges Rendern (SSR):
Der Startinhalt wird auf dem Server generiert, sodass der Browser die Daten als HTML-Seite laden kann. Updates werden jedoch weiterhin im Browser gehandhabt [LASN18].
Zur Beantwortung der Frage, ob ein serverseitiges Rendern verwendet werden sollte, bietet diese Webseite einen guten Überblick.
Vorteile:
- SEO (Search Engine Optimization) Verbesserung möglich (Google, Bing etc.)
- In der Regel bessere clientseitige Performance
Nachteile:
- Der Server wird stärker beansprucht, wodurch HTTP Antworten verzögert werden können
- Die Größe der HTML-Datei steigt und diese braucht somit länger zum Downloaden (i.d.R. vernachlässigbar)
- Die Komplexität der Anwendung nimmt zu
Zur Vereinfachung von SSR in React-Applikationen, kann das Framework Next.js verwendet werden. Hierbei wird zudem ein automatisches Code-Splitting durchgeführt.
Alternativ zu SSR kann auch prerender verwendet werden. Hierbei handelt es sich um eine Middleware, die Crawlern eine vorgerenderte Version der Webseite für eine bessere SEO zurückliefert [LASN18].
React Technologien und Konzepte
Den Abschluss des React-Frontend-Kapitels sollen React spezifische Technologien und Konzepte darstellen. Angefangen mit einer Einführung in die Entwicklung von Komponenten, sollen grundlegende React-Kenntnisse aufgebaut werden. Bestandteil dieses Abschnittes sind unter anderem die verschiedenen Arten von Komponenten - inklusive des Patterns "Higher Order Component" - sowie des bedingten Rendern und des internen Lifecycles von React-Komponenten. Im Anschluss werden einige Möglichkeiten des State Managements und Type Checkings innerhalb von React Anwendungen vorgestellt. Abschließend folgen weiterführende React-Themen in Form der Reconciliation (der Aktualisierungsstrategie des virtuell aufgebauten DOMs), der Error Boundaries, der Routing Möglichkeiten mit React sowie des Strict Modes.
Komponenten
Bei der Entwicklung von Webanwendungen mit React spielen Komponenten eine zentrale Rolle. Im Folgenden werden deshalb einige Grundlagen der Komponentenentwicklung mit React vorgestellt.
Arten
Grundsätzlich unterscheidet man zwei Arten von Komponenten. Die sogenannten "Dumb Components" und die "Smart Components". Zusätzlich werden in diesem Abschnitt noch die Higher Order Components vorgestellt, die genau genommen ein Entwurfsmuster für Komponenten darstellen.
Dumb Components
Dumb Components verfügen über keinen Zustand (state). Das heißt, dass sie einmalig Eigenschaften zugewiesen bekommen (properties, im Folgenden auch props genannt) und auf Basis dieser Werte ihr Aussehen anpassen.
Anmerkung: Es ist darauf zu achten, dass JavaScript standardmäßig über keine Typprüfung verfügt und somit auch die Verwendung der Properties ein sorgsames Vorgehen verlangt. Möglichkeiten der statischen Typprüfung werden im Kapitel Type Checking und Static Types vorgestellt.
Das nachfolgende Beispiel zeigt eine beispielhafte Dumb Component.
const Header = (props) => {
return (
<div>
{ props.title }
</div>
)
}
Die Komponente wird in der Variable "Header" als Arrow Function gespeichert. Die Funktion erhält die Properties in Form eines JavaScript Objekts als Parameter. Die übergebenen Eigenschaften werden dort als Key-Value Paare (props
) bereitgestellt. Im obigen Beispiel wird der Wert des Schlüssels title
innerhalb eines div-Containers angezeigt.
Die Properties werden im JSX als Attribut angelegt:
<Header title="Informatik Minden" />
/// Das hierzu gehörige Property-Objekt sieht folgendermaßen aus:
/// const props = {
/// title: "Informatik Minden"
/// }
Smart Components
Im Gegensatz zu Dumb Components verfügen Smart Components über einen Zustand (this.state
). Hierbei handelt es sich wie bei den Properties (this.props
) um ein Objekt, das aus Key-Value Paaren besteht. Besonders ist hierbei jedoch, dass sich der Zustand innerhalb des Lebenszyklus der Komponente ändern kann.
Smart Components werden in Form von Klassen realisiert. Ihre Properties erhalten sie dabei als Parameter ihres Konstruktors. Die folgende Komponente stellt einen Zähler dar, der als Property einen Startwert (props.startValue
) erhält und bei Betätigung des Buttons den Zähler um eins erhöht. Dieser Zähler wird als Zustandsvariable (this.state.count
) realisiert und mithilfe der Rendermethode angezeigt (die Rendermethode wird im Kapitel Rendern genauer betrachtet).
class Counter extends React.Component {
constructor(props) {
// Aufruf des Konstruktors von React.Component
super(props);
// der Startwert des Zählers muss als Property übergeben werden
this.state = { count: props.startValue };
// diese "Bindung" wird später erklärt; bitte erst einmal ignorieren
this.increment = this.increment.bind(this);
}
// Zählerzustand um 1 erhöhen
increment() {
this.setState({
count: this.state.count + 1
})
}
// Wird automatisch aufgerufen, wenn die Komponente gerendert werden soll
render() {
return (
<div>
<button onClick={this.increment}>+1</button>
<h1>Aktueller Wert: {this.state.count}</h1>
</div>
);
}
}
Bei der Verwendung des Zustands ist zu beachten, dass Werte von Zustandsvariablen mit der asynchronen Methode setState()
geändert werden müssen, damit die Änderungen korrekt durchgeführt werden. Die Methode erhält ein Objekt mit den Key-Value Paaren, die geändert werden sollen. Auf diese Art und Weise wird die Anzeige direkt nach dem Ändern eines Zustandswertes aktualisiert. Das Kapitel State Management in React beschreibt weitere Details zur Verwendung von setState()
und alternative Möglichkeiten zum Verwalten des Zustands von React-Komponenten. Informationen zum Lifecycle von Smart Components finden sich im Kapitel Lifecycle.
Higher Order Components (HOCs)
Higher Order Components erweitern Komponenten, indem sie ihnen zusätzliche Funktionalitäten oder Properties zur Verfügung stellen (vgl. Decorator Pattern der GoF) [TSON18]. Möchte man beispielsweise bei jedem Mounting- und Rendervorgang von Komponenten eine Log-Nachricht ausgeben, müsste man den gleichen Code an vielen Stellen des Projekts einbauen (in den Methoden componentDidMount()
und render()
der jeweiligen Komponente). Eine HOC kann hier Abhilfe schaffen:
// HOC
function withLogger(WrappedComponent) {
// Liefert eine neue Klasse zurück, die die übergebene
// Komponente WrappedComponent "umschließt"
return class extends React.Component {
// Wird aufgerufen, wenn die Komponente zum DOM hinzugefügt wird
componentDidMount() {
console.log(this.props.prefix + ' component did mount')
}
// Wird aufgerufen, wenn die Komponente gerendert wird
render() {
console.log(this.props.prefix + ' render')
// Mit "{...this.props}" werden alle Properties des HOCs kopiert,
// sodass die erweiterte Komponente diese erhält.
// Es können auch weitere Properties hinzugefügt werden.
// Weitere Infos hierzu: Nach "Spread-Operator" suchen
return (
<WrappedComponent {...this.props}> />
)
}
}
};
Bei der HOC handelt es sich um eine Art Factory-Methode, die als Parameter die zu erweiternde Komponente erhält und auf Basis dieser die high-order Komponente erstellt.
Dementsprechend wird die Funktion wie folgt aufgerufen:
// Beispielhafte Dumb Component, die erweitert werden soll
const Title = (props) => <h1>{props.title}</h1>;
// Aufruf der HOC
const LoggedComponent = withLogger(Title);
Die so erstellte Komponente kann anschließend wie üblich verwendet werden:
<LoggedComponent
prefix="LoggedComponent:" // Property für den Logger
title="HOC Test" // Property für die Komponente
/>
Es wird außerdem deutlich, dass hierdurch auch die Logik und die Anzeige besser voneinander getrennt werden können, wodurch die Wiederverwendbarkeit der einzelnen Teile verbessert wird. Die eigentliche Anzeige kann hierdurch oftmals durch Dumb Components realisiert werden. Es ist z.B. denkbar, dass eine HOC Daten aus dem Netz abruft und sie diese dann einer zustandslosen "Anzeigekomponente" zur Verfügung stellt [CHAN17]. Ebenso könnte eine HOC die Konfiguration einer anderen Komponente übernehmen. All das erleichtert zudem das Testen der Komponenten, da die Aufgaben strikter getrennt werden und dadurch Mock-Komponenten einfacher erstellt werden können [TSON18].
Children
Die vorherigen Beispiele haben bereits gezeigt, dass die grafische Oberfläche von React-Anwendungen aus hierarchisch strukturierten Elementen besteht. Innerhalb von Komponenten kann auf deren direkte Nachfahren mithilfe von props.children
(Dumb Component) bzw. this.props.children
(Smart Component) zugegriffen werden.
Eine Liste von Items kann beispielswiese wie folgt realisiert werden:
const List = (props) => {
return (
<div>
<p>Anzahl an Items: {React.Children.count(props.children)}</p>
<ul>
{props.children}
</ul>
</div>
)
}
Dieser Komponente können im Aufruf beliebig viele Kind-Elemente hinzugefügt werden:
<List>
<li>Item 1</li>
<li>Item 2</li>
</List>
//======================
// Ausgabe:
//======================
// Anzahl an Items: 2
// • Item 1
// • Item 2
//======================
Rendern
Wie bereits der Name vermuten lässt, ist render()
die Rendermethode von Smart Components. Sie wird aufgerufen, wenn die Komponente im Browser dargestellt werden soll (siehe Kapitel Lifecycle). Als Rückgabewert erhält sie genau ein React Element.
Ist es erforderlich, dass mehrere Elemente zurückgegeben werden, müssen diese in einem Container (z.B. <div>
) gebündelt werden.
Beispiel:
render() {
return (
<div> { /* zwingend erforderlicher Container */ }
<ChildA />
<ChildB />
<ChildC />
</div>
);
}
Hieraus resultiert ein weiteres Element im DOM (ein <div>
Element).
Diesen Umstand kann man durch Verwendung von Fragments umgehen. Sie gruppieren die Elemente, ohne selber im DOM eingefügt zu werden [FACE18d].
render() {
return (
<React.Fragment> { /* erscheint nicht im DOM */ }
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
// Oder mit der neuen Schreibweise: <> und </>
// (wird noch nicht von allen Tools unterstützt)
render() {
return (
<> { /* erscheint nicht im DOM */ }
<ChildA />
<ChildB />
<ChildC />
</>
);
}
Um Komponenten letztendlich im Browser anzuzeigen, wird die Funktion ReactDOM.render()
verwendet. Diese erhält als ersten Parameter die zu rendernde React Komponente und als zweiten Parameter das tatsächliche DOM-Element (Containers), als dessen Kind-Element die Komponente in den nativen DOM eingefügt worden soll:
ReactDOM.render(
<AppComponent />, // React Komponente
document.getElementById('root') // Container-Position im DOM
);
Bedingtes Rendern
Aufgrund des in JSX - also JavaScript - eingebetteten Codes für das Rendern von Komponenten, kann ein bedingtes Rendern relativ einfach umgesetzt werden. Im folgenden Beispiel wird, abhängig vom Zustand der Komponente, entweder ein "Start"- oder ein "Stopp"-Button angezeigt.
class StartStopButton extends React.Component {
constructor(props) {
super(props);
this.state = { isRunning: false };
// kann erst mal ignoriert werden
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
}
start() {
this.setState({ isRunning: true });
}
stop() {
this.setState({ isRunning: false });
}
render() {
return (
{/* hier findet die "Entscheidung" statt */}
this.state.isRunning
?
{/* wird gerendert, falls this.state.isRunning true ist */}
<button onClick={this.stop}>
Stopp
</button>
:
{/* wird gerendert, falls this.state.isRunning false ist */}
<button onClick={this.start}>
Start
</button>
);
}
}
Statt des hier verwendeten Konditionaloperator (... ? ... : ...
) kann auch z.B. ein if
verwendet werden. In diesem Fall darf die entsprechende Logik jedoch nicht "inline" in das return
-Statement eingebaut werden, sondern muss als separater Code davor eingefügt werden.
Beispiel:
// ...
render() {
let button; // Rückgabewert, enthält im Folgenden das
// anzuzeigende Element
if (this.state.isRunning) {
button = (
<button onClick={this.stop}>
Stopp
</button>
);
} else {
button = (
<button onClick={this.start}>
Start
</button>
);
}
return button;
}
}
//...
Events
Im Folgenden wird eine beispielhafte Komponente betrachtet, die aus einem Button besteht, der bei Aktivierung (Betätigung der Schaltfläche) eine Funktion mit dem Namen "start" aufruft.
Wie bereits in den vorherigen Beispielen ersichtlich, erfordert die Verwendung von UI-Komponenten oftmals eine ereignisbasierte Interaktion mit den einzelnen Elementen. Bei React Elementen werden die Events ähnlich wie bei herkömmlichen DOM Elementen gehandhabt.
HTML:
<button onclick="start()">
Start
</button>
JSX:
<button onClick={start}>
Start
</button>
In diesem Beispiel sind prinzipiell zwei Unterschiede zu erkennen:
- Die Eventnamen entsprechen den herkömmlichen Bezeichnungen in camelCase Schreibweise
- Die verbundenen Event-Handler werden als
{Expression}
(siehe JSX Kapitel) eingebunden
Meistens werden die Event-Handler innerhalb einer Klassenkomponente in Form von Klassenmethoden realisiert. Soll innerhalb des Handlers this
in Bezug auf die aktuelle Klasse verwendet werden, muss sichergestellt werden, dass die Klassenmethode an die Klasse gebunden wird (man spricht von "bound"). Wird eine Funktion nicht gebunden, bezieht sich this
in JavaScript i.d.R. auf den Aufrufer.
React bietet verschiedene Möglichkeiten dieser Problematik entgegenzuwirken [FACE18c].
Bindung im Konstruktor
class StartButton extends React.Component {
constructor(props) {
super(props);
// Hier wird die Methode an diese Komponente gebunden
this.start = this.start.bind(this);
}
start() {
this.doStuff(); // "this" kann hier normal verwendet werden
}
// ...
render() {
return (
<button onClick={this.start}>
Start
</button>
);
}
}
Public class field Syntax (experimentelles Feature)
class StartButton extends React.Component {
// !!! Experimentelles Feature !!!
// Babel konvertiert diese Funktion zu einer öffentlichen Klassenmethode.
// Je nach Projekteinstellung ist diese Funktion ggf. standardmäßig nicht aktiviert
start = () => {
this.doStuff(); // "this" kann hier normal verwendet werden
}
// ...
render() {
return (
<button onClick={this.start}>
Start
</button>
);
}
}
Arrow Function (nicht empfohlen)
Arrow Functions verfügen in JavaScript nicht über ein eigenes this
und umgehen somit die angesprochene Problematik [MOZI18]. Diese Art der Programmierung wird jedoch nicht empfohlen, da jeder Rendervorgang der Komponente eine neue Funktion erzeugt. Wird dieser Callback als Property an andere Komponenten weitergegeben, werden diese eventuell ebenfalls neu gerendert , wodurch die Performance leidet.
class StartButton extends React.Component {
start() = {
this.doStuff(); // "this" kann hier verwendet werden
}
// ...
render() {
return (
<button onClick={() => this.start()}>
Start
</button>
);
}
}
Den Callback-Methoden können ebenfalls Parameter mitgegeben werden [FACE18c]:
const someValue = 1;
// ...
<button onClick={(event) => this.start(someValue, event)}>Start</button>
// oder
<button onClick={this.start.bind(this, someValue)}>Start</button>
In beiden Fällen wird das React Event als zweiter Parameter an die Methode start()
übergeben. Werden bei bind()
mehrere Parameter übergeben, wird der Eventparameter entsprechend nach hinten verschoben. Die Parameterübergabe geschieht dort implizit und muss somit nicht angegeben werden.
Styling
Das Styling von React Komponenten kann prinzipiell auf unterschiedliche Arten geschehen. Im Folgenden werden drei häufig verwendete Verfahren vorgestellt.
CSS
Das Aussehen von Elementen kann z.B. direkt über herkömmliche CSS-Dateien angepasst werden.
Es ist darauf zu achten, dass das class
-Attribut in JSX className
lautet.
Das id
-Attribut kann wie von HTML gewohnt verwendet werden.
Beispiel:
.css
.red-text {
color: red;
}
#some-id {
font-size: 12px;
}
.jsx
function someComponent() {
return <div className="red-text" id="some-id">Roter Text</div>;
}
Bei der Verwendung von CSS ist darauf zu achten, dass die Style-Definitionen standardmäßig global definiert werden, wodurch unerwünschte Seiteneffekte entstehen können.
Two CSS properties walk into a bar. A barstool in a completely different bar falls over. - Thomas Fuchs
In Verbindung mit den Bundle-Tools webpack oder browserify können auch CSS-Modules verwendet werden. Hierbei werden die Klassen- und Animationsnamen standardmäßig als lokal angelegt.
style-Attribut
Eine weitere Möglichkeit ist das Verwenden des Attributs style
. Dem Attribut kann ein JavaScript Objekt zugewiesen werden, dass über die aus CSS bekannten Attribute verfügt. Hierbei ist darauf zu achten, dass die Style-Bezeichner ihrem CSS-Äquivalent als camelCase entsprechen. So wird font-size
z.B. zu fontSize
. Außerdem müssen Werte, die keine reine Zahl sind, als String übergeben werden.
Beispiel:
const redTextStyle = {
color: 'red',
fontSize: '12px',
};
// ...
function someComponent() {
return <div style={redTextStyle}>Roter Text</div>;
}
Styled Components
Darüber hinaus kann z.B. auch die Bibliothek styled-components verwendet werden. Diese verwendet ebenfalls eine Mischung aus CSS und JavaScript. Als Kennung für eine Style-Beschreibung dient das Gravis-Symbol ( ` ) am Anfang und Ende des Styles. Hierbei wird eine String-Interpolation mit Tagged Template-Strings durchgeführt, wodurch eine React-Komponente erzeugt wird, die zum Rendern verwendet werden kann. Vorteilhaft ist hierbei, dass der Style lokal gehalten wird und somit direkt mit der jeweiligen Komponente verbunden wird. Die Bibliothek verfügt darüber hinaus über weitere Features, wie das Verwenden von Themes oder dem Verändern des Styles auf Basis von Properties.
Beispiel:
const RedText = styled.div`
color: red;
font-size: 12px;
`;
// ...
function someComponent() {
return <RedText>Roter Text</RedText>;
}
Lifecycle
Der Lebenszyklus einer React Klassenkomponente besteht aus verschiedenen Schritten und Phasen. Die nachfolgende Abbildung zeigt deren Zusammenhang zueinander. Es existieren noch weitere Lifecycle-Methoden. Da diese jedoch als veraltet gelten und mit der Version 17 entfernt werden, werden diese im Folgenden außer Acht gelassen. Als Informationsquelle für diesen Abschnitt wird, sofern nicht anders angegeben, [FACE18b] verwendet.
Abbildung bearbeitet; entnommen aus [MAJ18] (dort als interaktive Abbildung vorhanden)
Mounting
Noch bevor die Komponente zum DOM hinzugefügt wurde (man spricht hierbei von "mounted"), wird ihr Konstruktor (constructor(props)
) aufgerufen. Hierbei sollte darauf geachtet werden, dass als erstes der Konstruktor der Basisklasse aufgerufen wird (super(props)
), da andernfalls das Klassenattribut this.props
noch nicht definiert ist und es somit zu Bugs kommen kann. Der Konstruktor sollte zur Initialisierung des Objektzustands verwendet werden (mit this.state={...}
; hier nicht setState()
verwenden, damit der Wert direkt gesetzt wird und kein erneuter Rendervorgang ausgelöst wird).
Nachdem das Objekt initialisiert wurde, wird die Methode getDerivedStateFromProps(nextProps, prevState)
aufgerufen. Als Rückgabewert wird ein Objekt erwartet, das den Zustand auf Basis der angepassten Properties enthält oder null
, um zu signalisieren, dass sich der Zustand auf Basis der Props nicht geändert hat. Um zu prüfen, welche Props sich geändert haben (bzw. ändern werden), kann der Parameter nextProps
mit den aktuellen Properties des Objekts verglichen werden.
Es folgt der Aufruf von render()
und anschließend das Updaten des "nativen DOMs" und der "refs" (Verweise auf die Elemente des nativen DOMs).
Mit dem Aufruf von componentDidMount()
endet der Mounting-Vorgang. Innerhalb dieser Methode können beispielsweise Initialisierungen eingebaut werden, die Zugriff auf den nativen DOM benötigen oder Netzwerkzugriffe durchgeführt werden (AJAX-Requests). Obwohl render()
bereits ausgeführt wurde, kann der Zustand hier jedoch noch mit setState()
aktualisiert werden, bevor die Änderungen tatsächlich im Browser sichtbar werden (render()
würde in dem Fall erneut aufgerufen werden; Achtung, die Performanz kann hierunter leiden).
Updating
React Komponenten können in ihrem Lebenszyklus auf 3 verschiedene Arten aktualisiert werden.
New props
Sie können von ihrem hierarchischen Vater (bezogen auf die ReactDOM-Hierarchie) neue Props erhalten. In diesem Fall wird die Methode getDerivedStateFromProps()
(s.o.) erneut aufgerufen. Der Unterschied zum Aufruf während des Mounting-Vorgangs ist, dass nun vor render()
eine weitere Methode aufgerufen wird, mithilfe der entschieden werden kann, ob ein erneutes Rendern wirklich erforderlich ist. Es handelt sich um die Methode shouldComponentUpdate(nextProps, nextState)
. Standardmäßig liefert sie true
zurück, sodass der Rendervorgang durchgeführt wird. Implementiert man diese Methode und gibt false
zurück (z.B. weil die neu erhaltenen Props keine Relevanz für die Anzeige haben), wird der Update-Vorgang ohne Rendern abgeschlossen.
setState()
Ein durch setState()
geänderter Zustand ruft shouldComponentUpdate()
ebenfalls auf.
forceUpdate()
Ein Aufruf der Methode forceUpdate()
initiiert ebenfalls ein erneutes Rendern. Nach Möglichkeit sollte forceUpdate()
nur aufgerufen werden, wenn eine GUI-Änderung andernfalls nicht bemerkt werden würde (das sollte im Normallfall jedoch nicht notwendig sein).
Bevor die Änderungen in den nativen DOM übertragen werden, wird die Methode getSnapshotBeforeUpdate(prevProps, prevState)
aufgerufen. Hiermit können Werte gespeichert werden, die sich potentiell nach dem Update ändern könnten (z.B. eine Scrollbar-Position). Alle Bestandteile des Rückgabewertes werden an die Methode componentDidUpdate(prevProps, prevState[, snapshot])
weitergeleitet. Diese Methode wird demnach aufgerufen, nachdem die Änderungen im nativen DOM angepasst wurden.
Unmounting
Unmittelbar bevor eine Komponente unmountet und zerstört wird, wird die Methode componentWillUnmount()
aufgerufen. Sie dient zum Aufräumen von Timern, Netzwerkverbindungen etc.
Abschlussbemerkung
Zusätzlich zu den hier vorgestellten Lifecycle-Methoden existiert auch noch die Methode componentDidCatch(error, errorInfo)
. Diese erzeugt aus der aktuellen Klasse eine Error Boundary Klasse, die JavaScript-Fehler innerhalb ihrer Kinderelemente fängt. Weiterführende Informationen finden sich im Kapitel Error Boundaries.
State Management in React
Wie bereits im Kapitel State Management erläutert, bedarf das Erstellen von dynamischen Webseiten einer guten Strategie zur Verwaltung des Zustandes der Webseite. Im Folgenden werden einige Möglichkeiten des State Managements einer React-Anwendung vorgestellt.
setState
Eine einsteigerfreundliche Variante des State Managements ist das Verwenden der bereits in vorherigen Kapiteln angesprochenen asynchronen Methode setState()
. Jede smarte Komponente verfügt über einen Zustand. Dieser Zustand wird als JavaScript-Objekt in dem Klassenattribut this.state
gespeichert und mithilfe von setState()
aktualisiert (vgl. Kapitel Dumb Components und Smart Components). Mit steigender Anwendungsgröße steigt auch die Komplexität eines solchen direkten State Managements. Das rührt daher, dass die Verbindungen (Bindings) explizit als Property bis an die untersten Komponenten weitergeführt werden müssen, um dort aktiv zu werden [LABO18].
Context
Um ein umständliches Weiterreichen der Statusinformationen in Form von Properties aus dem Weg zu gehen, verfügen die neusten React Versionen über das kontextbasierte Verfahren Context. Es sollte dort genutzt werden, wo bestimmte Daten als global angesehen werden können (z.B. Benutzerdaten und GUI-Themes).
Das Verfahren verwendet einen "Provider & Consumer" Ansatz (Anbieter und Verbraucher). Die Provider verfügen über Daten, die von Consumern abonniert werden können. Letztere werden über Änderungen der abonnierten Daten informiert.
Die Verwendung von Context ist recht simpel [FACE18f]:
Schritt 1: Kontext erzeugen
// Erzeugt ein Kontextpaar: {Provider, Consumer}
// defaultValue wird von Consumern verwendet, die keinen
// passenden Provider im JSX-Baum "über" sich haben
const MyContext = React.createContext(defaultValue);
Schritt 2: Kontext verwenden
<div>
// Kontext setzen
<MyContext.Provider value={someContextValue}>
...
<MyContext.Consumer> {
contextValue => (
// Hier das Element rendern, das auf den Wert des Kontextes angewiesen ist.
// Der Kontext kann über "contextValue" gelesen werden
)
}
</MyContext.Consumer>
...
</MyContext.Provider>
</div>
MobX
Eine weitere Möglichkeit des State Managements bietet die JavaScript Bibliothek MobX. Sie basiert auf Observer und Observables, die auch aus dem Observer Pattern bekannt sind. MobX verfügt über einen unidirektionalen Datenfluss, der in etwa wie Flux abläuft:
Abbildung bearbeitet; entnommen aus [PURN16]
Events triggern Actions, die den State modifizieren. Auf Basis dessen werden wiederum Computed Values automatisch berechnet und die Änderungen der View entsprechend bekanntgemacht. Es ist jedoch auch möglich, einen State direkt mit der View zu verknüpfen. Die View kann selber auch Events zum triggern von Actions auslösen.
Das Verwenden dieser Bibliothek ist recht einfach [MOBX18]:
Schritt 1: Zustandsobjekt definieren und als observable kennzeichnen
import {observable} from 'mobx';
var counterState = observable({
value: 0
});
Schritt 2: Eine View als Observer erstellen
import {observer} from 'mobx-react';
// ESnext Decorator
// Syntax ohne Decorator:
// "export observer(CounterView)" am Ende der Datei
@observer
class CounterView extends React.Component {
constructor(props) {
super(props);
this.increment = this.increment.bind(this);
}
render() {
return (
<div>
<button onClick={this.increment}>+1</button>
<h1>Aktueller Wert: {this.props.counterState.value}</h1>
</div>
);
}
increment () {
// siehe Schritt 3
this.props.counterState.increment();
}
};
Schritt 3: Zustandsobjekt modifizieren
import {action} from 'mobx';
counterState.increment = action(() => {
counterState.value += 1;
});
Schritt 4: Komponente und Zustandsobjekt verbinden
// "counterState" wird als normale Property verwendet
ReactDOM.render(<Counter counterState={this.props.counterState} />, document.body);
Zur Debug-Unterstützung kann die Browser-Extension MobX Developer Tools genutzt werden.
Redux
Redux ist eine weitere JavaScript Bibliothek, die (i.d.R. genau) einen "Zustands-Container" (state-container) zur Hilfe bei der Verwaltung des Datenflusses einer Applikation bereitstellt. Die Architektur erinnert ebenfalls an die Flux Architektur, siehe nachfolgende Abbildung. Für die Verwendung von Redux mit React kann man die Bibliothek react-redux verwenden.
Abbildung entnommen aus [TSON18]
Redux besteht aus vier Bestandteilen [TSON18]:
Action
Die Actions verhalten sich analog zu den Actions des Flux Patterns, jedoch ist ihr Aufbau leicht abgeändert. Statt des Attributs "payload" werden die Nutzdaten direkt an das Action-Objekt angefügt:
{
type: 'ADD_PERSON',
forename: 'Max'
surname: 'Mustermann'
}
Randbemerkung:
Es wird empfohlen möglichst wenig Nutzdaten in eine Aktion einzubinden. Statt der gesamtem Informationen kann beispielsweise ein Index übergeben werden, mithilfe dessen die Informationen aus einer Liste o.ä. entnommen werden können [REDU18].
Reducer
Da Redux i.A. nur über einen State-Container verfügt, wird kein Dispatcher benötigt. Stattdessen verwendet Redux Reducer. Dies sind Funktionen, die den aktuellen Zustand eines Stores sowie eine Action erhalten und auf Basis dieser Informationen den neuen Zustand zurückgeben. Reducer müssen zwingend die folgenden beiden Eigenschaften erfüllen, damit das zugrundeliegende Pattern von Redux nicht verletzt wird:
Es muss sich um eine sogenannte Pure Function handeln, d.h. der Ausgabewert der Funktion ist bei gleicher Eingabe immer gleich
Die Funktion darf keine Seiteneffekte besitzen, d.h. keine globalen Variablen verwenden, keine asynchronen Aufrufe tätigen etc.
Das folgende Beispiel stellt einen einfachen Reducer für ein Array von Personen dar. Es erhält den aktuellen Zustand (ein Array mit den aktuell vorhandenen Personen) sowie die auszuführende Action. Anhand des Action-Attributes type wird die auf den Zustand anzuwendende Aktion ausgewählt und der neue Zustand anschließend zurückgegeben (als tiefe Kopie!). Der Zustand selber ist somit immutable und kann z.B. mithilfe der Browser-Erweiterung Redux DevTools komfortabel gedebuggt werden. Speichert man sich die tiefen Kopien als Historie ab, kann man auf einfache Art und Weise "Undo-Funktionalitäten" realisieren.
Aus Gründen der Übersichtlichkeit wird in dem Beispiel von dem aktuellen Zustand state
jedoch keine tiefe Kopie erzeugt. Dies müsste bei einer Implementation entsprechend angepasst werden.
const personReducer = (state, action) => {
switch (action.type) {
case 'ADD_PERSON':
return [
// Die bisherigen Personen
// Achtung, keine tiefe Kopie!
...state,
// Neue Person anfügen
{
forename: action.forename,
surname: action.surname
}
]
case 'REMOVE_PERSON':
//...
// case ...
// Sonstige Actions
// ...
default:
// Fehlerbehandlung...
}
}
React (View):
Die Anzeige; analog zu Flux.
Store
Das Model; prinzipiell analog zu Flux, jedoch als "Single-Store". Verwendet Reducer für seine Zustandsübergänge.
Auswahlhilfe
Neben den erwähnten Möglichkeiten existieren viele weitere Bibliotheken für das Verwalten von Zuständen. Die Auswahl des richtigen Werkzeugs ist dementsprechend nicht immer einfach.
Die folgenden Fragen sollen die Auswahl für die vorgestellten Verfahren vereinfachen [LABO18]:
Soll es keine externen Abhängigkeiten geben?
setState oder Context verwenden
Steht die Einfachheit im Vordergrund?
MobX verwenden
Ist die Skalierbarkeit wichtig?
Redux verwenden
Type Checking und Static Types
Standardmäßig ist JavaScript eine dynamisch typisierte Programmiersprache. Möchte man dennoch Type-Checking-Methodiken oder statische Typisierung in JavaScript verwenden, bedarf es zusätzlicher Bibliotheken/Tools. Im Folgenden werden in diesem Zusammenhang PropTypes, Flow und TypeScript vorgestellt.
PropTypes
Durch das Importieren des Packets "prop-types" erhält man Zugriff auf das in React eingebaute Type Checking Werkzeug PropTypes. Mithilfe von PropTypes kann überwacht werden, ob alle benötigten Properties einer Komponente übergeben wurden und ob der Typ des Übergabewertes korrekt ist.
Im folgenden Beispiel wird ein zwingend notwender String title
sowie optionale Kindelemente children
als Properties festgelegt.
Hierzu wird der Komponenten eine Variable mit dem Namen propTypes
zugewiesen. Innerhalb dieser Variable werden die Properties und deren Typen definiert. Das Anfügen von isRequired
führt dazu, dass der Property zwingend ein Wert des angegebenen Typs zugewiesen werden muss.
Ist dies nicht der Fall, erscheint eine entsprechende Fehlermeldung in der Konsole des verwendeten Browsers. Ebenso erscheint eine Fehlermeldung, wenn einer Property ein Wert falschen Typs zugewiesen wurde.
import PropTypes from 'prop-types';
class Header extends React.Component {
render() {
return (
<div>
<h1>{this.props.title}</h1>
{this.props.children}
</div>
);
}
}
Header.propTypes = {
title: PropTypes.string.isRequired, // zwingend notwendig
children: PropTypes.element // optional
};
Es können viele Typen überprüft werden, z.B.:
PropTypes.string
PropTypes.number
PropTypes.array
PropTypes.bool
PropTypes.func
-> FunktionPropTypes.element
-> React ElementPropTypes.instanceOf(<Klasse>)
PropTypes.oneOf(['EnumA', 'EnumB'])
-> wie eine Enumeration
PropTypes überprüft die Typen aus Gründen der Performance nur im "development mode" [FACE18e].
Flow
Flow ist ein Static Type Checker für JavaScript Applikationen. Er kann zum Ersetzen von PropTypes genutzt werden. Darüber hinaus können mit Flow jedoch nicht nur die Properties, sondern auch z.B. die Typen von Parametern und Rückgabewerten überprüft werden [CLAR17].
Damit eine Datei von Flow überprüft wird, muss sie folgendermaßen markiert werden:
// @flow
Der im letzten Kapitel vorgestellte PropTypes Code kann beispielsweise wie folgt mithilfe von Flow ersetzt werden:
// @flow
type Props = {
title: string, // zwingend notwendig
children?: React.Node, // optional
};
class Header extends React.Component<Props> {
render() {
return (
<div>
<h1>{this.props.title}</h1>
{this.props.children}
</div>
);
}
}
Überprüfung von Parametern:
// @flow
function myLoggerFunction(text: string): void {
console.log(text)
}
myLoggerFunction(2); // Fehler
Flow analysiert den Quellcode auch ohne das explizite Angeben von Typen:
// @flow
function add(var1, var2) {
return var1 + var2; // Fehler
}
const a = add("1", "100");
TypeScript
TypeScript ist eine von Microsoft entwickelte Open Source Programmiersprache. Die Sprache verfügt über vielerlei Bestandteile, die aus anderen Programmiersprachen bekannt sind. Hierzu gehören Generics, Vererbung, Klassen, Interfaces, Enumerationen und vieles mehr. Ein spezieller TypeScript-Compiler kompiliert den Softwarecode zu nativen JavaScript Code. Da die JavaScript-Sprache lediglich erweitert wird, ist herkömmlicher JavaScript Code zu TypeScript kompatibel, wodurch sich JavaScript Projekte i.d.R. recht einfach migrieren lassen.
In TypeScript erhält jede Funktion, Variable, Schnittstelle etc. einen Typ zugewiesen. Hierdurch können viele Fehlerquellen bereits zur Kompilierzeit festgestellt werden. Darüber hinaus bieten einige Tools weitere Hilfestellungen bei der Programmierung an (z.B. das automatische Erstellen von Methodenrümpfen, die für eine Implementierung eines Interfaces notwendig sind).
Beispielhafte Umsetzung eines Timer Interfaces und deren Implementierung:
interface TimerInterface {
setTimer(value: number): void;
start(): void;
stop(): void;
onElapsed(): void;
isRunning(): boolean;
}
/// Implementierung analog wie z.B. in Java
class Timer implements TimerInterface {
setTimer(value: number): void {
// ...
}
start(): void {
// ...
}
stop(): void {
// ...
}
onElapsed(): void {
// ...
}
isRunning(): boolean {
// ...
}
}
Eine weitere wichtige Erweiterung sind die Zugriffsmodifizierer (access modifiers), die eine bessere Kapselung von Klassenfunktionalität ermöglichen. Es werden die drei klassischen Modifier unterstützt:
public
(default): von überall aus erreichbarprivate
: nur innerhalb der Klasse erreichbarprotected
: nur innerhalb der Klasse und über Vererbung erreichbar
Das Typensystem kann jedoch auch bei Verwendung von TypeScript umgangen werden. Wird als Typ any
angegeben, findet keine Überprüfung des Typs statt.
Um solche unsauberen Lösungen zu umgehen, kann z.B. das statische Code-Analyse Tool "TSLint" verwendet werden. Wird hier die Regel no-any
gesetzt, wird eine entsprechende Warnung bzw. ein Fehler ausgegeben. Dieses Tool kann außerdem dabei helfen, Coderichtlinien einzuhalten.
Einen schnellen Einstieg in die Entwicklung von React Anwendungen mit TypeScript und TSLint findet sich hier.
Reconciliation
Da das Updaten von Elementen des nativen DOMs relativ ineffizient ist, verfügt React über ein virtuelles DOM. Statt Änderungen direkt im DOM zu rendern, wird der alte virtuelle DOM mit dem aus den Änderungen resultierenden DOM verglichen. Hierbei handelt es sich um leichtgewichtige JavaScript Objekte, die in-memory gespeichert werden, sodass dort Operationen deutlich schneller durchgeführt werden können. Initial wird der virtuelle DOM mit der Funktion ReactDOM.render()
erstellt.
Der Objektbaum wird durch das Aufrufen von setState()
jeweils komplett neu erstellt und verglichen. Algorithmen, die die minimale Anzahl von nötigen Modifikation bestimmen, liegen bestenfalls in O(n³). Um diesen Vorgang zu beschleunigen, verwendet React den sogenannten Reconciliation-Algorithmus. Hierbei wird eine Heuristik verwendet, die das Problem in O(n) lösen kann. Es werden 2 Annahmen bzw. Vereinbarungen getroffen:
1) Annahme: Zwei Elemente unterschiedlichen Typs erzeugen unterschiedliche DOM-Bäume.
Es wird eine Breitensuche (ebenenweises Durchlaufen des Baums) durchgeführt, wobei jeweils die Typen der Elemente beider Bäume verglichen werden. Unterscheiden sich diese, werden die Elemente samt aller Kinder als dirty markiert. Komponenten, die als dirty markiert wurden, werden anschließend neu gerendert.
2)
Elemente können mit einem Schlüssel key
als Identifier versehen werden. Bei einer Liste von Elementen mit solchen Keys werden folgende Schritte durchgeführt [HOPP17]:
Vergleiche die alte Liste mit der neuen Liste:
- Durchlaufe alle Elemente
- Key in alter, aber nicht in neuer Liste vorhanden?
- rufe
unmount
für die Komponente auf
- rufe
- Key in neuer, aber nicht in alter Liste vorhanden?
- rufe
mount
Komponente auf
- rufe
- Key in beiden Listen vorhanden?
- rufe
shouldComponentUpdate
auf, um zu entscheiden, was passieren soll
- rufe
- Key in alter, aber nicht in neuer Liste vorhanden?
Anmerkung:
Seit ca. 2 Jahren arbeitet das React Team zudem an einer neuen Datenstruktur (React Fiber), die den aktuellen Algorithmus verbessern und ersetzen soll. Informationen hierzu finden sich hier.
Error Boundaries
Error Boundaries (dt. Fehlergrenzen) dienen dem Einfangen von JavaScript Fehlern innerhalb der Kindelemente. Wenn Kind-Komponenten abstürzen, können diese Fehler protokolliert oder alternative GUI-Element angezeigt werden. Die Fehler werden während des Renderns, in anderen Lifecycle Methoden und auch innerhalb von Konstruktoren aufgefangen.
Um aus einer Komponente eine Error Boundary Klasse zu machen, muss lediglich die folgende Lifecycle Methode implementiert werden:
componentDidCatch(error, errorInfo) {}
Die Methode erhält zwei Parameter:
- error: Der geworfene Fehler
- errorInfo: Objekt, das für Stacktrace Informationen die Variable
componentStack
bereithält
Pro:
- Folgt der deklarativen Natur von React (was soll passieren, statt wie)
- Error Boundary Klassen können einfach wiederverwendet werden
- Verhält sich wie zu erwarten: Fehler werden zur nächsthöheren Error Boundary Klasse weitergeleitet
Contra:
- ¯\(ツ)/¯ 😉
In diesem Beispiel wird eine mögliche Realisierung implementiert [GROF18].
React Router
Mit React werden hauptsächlich Single Page Applications erstellt. Aus diesem Grund ist die Webseite nicht darauf angewiesen für ihre Unterseiten verschiedene URLs zu verwenden. Solche URLs erlauben es dem Benutzer jedoch die bestimmten Seiten als Lesezeichen abzuspeichern und die Vorwärts- und Rückwärts-Pfeile des Browsers für die Navigation zu verwenden.
Damit Komponenten einer SPA trotzdem in verschiedene URL-Bereiche aufgeteilt werden können, kann bei React der react-router verwendet werden.
// ...
const DisplayText = ({ match }) => (
<div>
{match.params.text}
</div>
);
ReactDOM.render((
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/Display/:text" component={DisplayText} />
<Route component={Default} />
</Switch>
</BrowserRouter>
), document.getElementById('root'))
In diesem Beispiel würde ein Aufruf von "URL/" die Komponente Home
aufrufen, wohingegen "URL/Display/Hallo" den Wert Hallo als Parameter text
and die Komponente DisplayText
übergeben würde. In allen anderen Aufrufen der URL würde die Komponente Default
aufgerufen werden. Die Routen lassen sich auch Verschachteln, woraufhin tiefergelegene Komponenten entsprechend als children
an die darüberliegende Komponente übergeben werden. Dieser Routing-Vorgang wird aufgrund von <BrowserRouter>
dynamisch ausgeführt, d.h. er wird zur Laufzeit vorgenommen [SCHE17].
Für statische SPAs, bei denen der Server nur statische Dateien liefern kann, kann der HashRouter
anstelle von BrowserRouter
verwendet werden. Der HashRouter
verwendet den Hash (#) einer URL (window.location.hash
) zur Unterteilung. Es wird also nach einem Hash in der URL gesucht und der Text dahinter als Parameter verwendet [TECH17].
Strict Mode
Strict Mode stellt sicher, dass gewisse Best Practices eingehalten werden. Durch das Einfügen von StrictMode
in den ReactDOM
werden die Kinderelemente dieser Komponente auf verschiedene mögliche Probleme hin überprüft [SZCZ18]. Wird beispielsweise eine veraltete (deprecated) Lifecycle-Methode verwendet, erscheint eine Warnung der folgenden Art:
Abbildung entnommen aus [SZCZ18]
Implementierung:
// ...
// Versetze "ChildComponent" in den Strict Mode
render() {
return (
<StrictMode>
<ChildComponent />
</StrictMode>
)
}
// ...
Literaturverzeichnis
[AVRA14]: Avram, Abel: Facebook: MVC Does Not Scale, Use Flux Instead [Updated]. URL: https://www.infoq.com/news/2014/05/facebook-mvc-flux (abgerufen am 29.06.2018)
[CHAN17]: Tsonev, Krasimir: React Patterns. URL: https://reactpatterns.com/ (abgerufen am 09.05.2018)
[CLAR17]: Clark, Tyler: Quick Reference: React with Flow. URL: https://medium.com/@tylerwclark/quick-reference-react-with-flow-b404e5cd5d0e (abgerufen am 05.05.2018)
[FACE14]: Facebook : Hacker Way: Rethinking Web App Development at Facebook. URL: https://www.youtube.com/watch?v=nYkdrAPrdcw&feature=youtu.be&t=568 (abgerufen am 11.05.2018)
[FACE18a]: Facebook Inc.: Introducing JSX. URL: https://reactjs.org/docs/introducing-jsx.html (abgerufen am 05.05.2018)
[FACE18b]: Facebook Inc.: React.Component. URL: https://reactjs.org/docs/react-component.html (abgerufen am 05.05.2018)
[FACE18c]: Facebook Inc.: Handling Events. URL: https://reactjs.org/docs/handling-events.html (abgerufen am 06.05.2018)
[FACE18d]: Facebook Inc.: React.Component. URL: https://reactjs.org/docs/fragments.html (abgerufen am 06.05.2018)
[FACE18e]: Facebook Inc.: Typechecking With PropTypes. URL: https://reactjs.org/docs/typechecking-with-proptypes.html (abgerufen am 06.05.2018)
[FACE18f]: Facebook Inc.: Context. URL: https://reactjs.org/docs/context.html#provider (abgerufen am 06.05.2018)
[FACE18g]: Facebook Inc.: Code-Splitting. URL: https://reactjs.org/docs/code-splitting.html (abgerufen am 21.05.2018)
[GAUN18]: Gaunt, Matt: Service Workers: an Introduction. URL: https://developers.google.com/web/fundamentals/primers/service-workers/ (abgerufen am 22.05.2018)
[GOOG18a]: Google Developers: Progressive Web Apps. URL: https://developers.google.com/web/progressive-web-apps/ (abgerufen am 22.05.2018)
[GOOG18b]: Google Developers: Progressive Web App Checklist. URL: https://developers.google.com/web/progressive-web-apps/checklist (abgerufen am 22.05.2018)
[GROF18]: Groff, Sean: 2 Minutes to Learn React 16's componentDidCatch Lifecycle Method. URL: https://medium.com/@sgroff04/2-minutes-to-learn-react-16s-componentdidcatch-lifecycle-method-d1a69a1f753 (abgerufen am 22.05.2018)
[HOPP17]: Hopper, Grace: Tech Talk: What is the Virtual DOM?. URL: https://www.youtube.com/watch?v=d7pyEDqBDeE (abgerufen am 22.05.2018)
[LABO18]: Labori, Gant: The React State Museum. URL: https://hackernoon.com/the-react-state-museum-a278c726315 (abgerufen am 12.05.2018)
[LASN18]: Lasn, Indrek: Next.js — React Server Side Rendering Done Right. URL: https://hackernoon.com/next-js-react-server-side-rendering-done-right-f9700078a3b6 (abgerufen am 21.05.2018)
[MAJ18]: Maj, Wojciech: Interactive React lifecycle methods diagram. URL: https://github.com/wojtekmaj/react-lifecycle-methods-diagram (abgerufen am 05.05.2018)
[MOBX18]: MobX: The gist of MobX. URL: https://mobx.js.org/intro/overview.html (abgerufen am 12.05.2018)
[MOZI18]: Mozilla and individual contributors: Arrow functions. URL: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions (abgerufen am 06.05.2018)
[OSMA18]: Osmani, Addy: The App Shell Model. URL: https://developers.google.com/web/fundamentals/architecture/app-shell (abgerufen am 22.05.2018)
[PURN16]: Purnelle, Kevin: Handling React Forms with Mobx Observables. URL: https://blog.risingstack.com/handling-react-forms-with-mobx-observables/ (abgerufen am 28.06.2018)
[REDU18]: Redux: Actions. URL: https://redux.js.org/basics/actions (abgerufen am 11.05.2018)
[REAC18]: React Loadable: A higher order component for loading components with promises. URL: https://github.com/jamiebuilds/react-loadable (abgerufen am 21.05.2018)
[SCHE17]: Scherman, Paul : A Simple React Router v4 Tutorial. URL: https://medium.com/@pshrmn/a-simple-react-router-v4-tutorial-7f23ff27adf (abgerufen am 21.05.2018)
[SZCZ18]: Szczeciński, Bartosz: What’s new in React 16.3(.0-alpha). URL: https://medium.com/@baphemot/whats-new-in-react-16-3-d2c9b7b6193b (abgerufen am 21.05.2018)
[TECH17]: Techiediaries: React Router DOM v4 Tutorial (with Examples). URL: https://legacy.gitbook.com/book/krasimir/react-in-patterns/details (abgerufen am 22.05.2018)
[TSON18]: Tsonev, Krasimir: React in patterns. URL: https://legacy.gitbook.com/book/krasimir/react-in-patterns/details (abgerufen am 09.05.2018)
[WASS13]: Wasson, Mike: ASP.NET - Single-Page Applications: Build Modern, Responsive Web Apps with ASP.NET. URL: https://msdn.microsoft.com/en-us/magazine/dn463786.aspx (abgerufen am 22.05.2018)
[ZEIG16]: Zeigermann, Oliver ; Hartmann, Nils: React : Die praktische Einführung in React, React Router und Redux. 1. Aufl. s.l. : dpunkt, 2016