zurück zum Artikel

JavaScript: Einführung in React

Werkzeuge
JavaScript: Einführung in React

React ist ein von Facebook entwickeltes JavaScript-Framework, das von einer ständig wachsenden Community gepflegt wird. Wegen der Möglichkeit, React Views sowohl auf dem Client, als auch auf dem Server zu rendern, erfreut sich das Framework bei vielen Entwicklern großer Beliebtheit.

Obwohl bereits andere Frameworks den Weg eines isomorphen Renderings (Client/Server) gegangen sind, unterscheidet sich React von ihnen in einer grundlegenden Funktion: dem virtuellen Document Object Model (DOM). Die Views modifizieren nicht direkt den DOM des Browsers, sondern halten ihn virtuell in den Komponenten vor und sind damit komplett von irgendwelchen Abhängigkeiten losgelöst. Mit Methoden wie renderToString(<React View Component>) lassen sich die React-View-Komponenten auf dem Server rendern.

Dass React funktioniert, haben einige große Unternehmen bereits gezeigt: Nicht nur Teile von Facebook wurden damit entwickelt, sondern auch Plattformen wie Instagram oder die Webversion von WhatsApp nutzen das Framework. Gerade für Seiten, die in Suchmaschinen eine Rolle spielen sollen, ist Search Engine Optimization, kurz SEO, ein KO-Kriterium. Daher ist zu beachten, dass das serverseitige Rendern von Webanwendungen, die mit Frameworks wie AngularJS entwickelt wurden, sehr aufwendig ist. Beispielsweise ist das Erstellen von Snapshots des HTML-Codes mit PhantomJS bei dynamischen Seiten nicht gerade einfach – und auch nicht besonders performant.

In React hingegen erzeugen alle Komponenten ihr eigenes virtuelles DOM. Dadurch lassen sie sich nicht nur auf dem Server rendern, sondern sie erleichtern auch das Unit-Testing. Wenn sich Komponenten beispielsweise durch Benutzerinteraktionen oder durch neue Antworten vom Server ändern, versucht das virtuelle DOM nicht die ganze Anwendung, sondern nur das kleinste betroffene Element neu zu rendern.

Ein Beispiel kann den Performance-Gewinn deutlicher erklären: Angenommen es gibt ein Model (siehe Model View Controller), das etwa ein Bürogebäude abbilden soll. Alle relevanten Eigenschaften des Gebäudes (Räume, Fenster ...) werden auf den aktuellen State des Objektes gespiegelt. Ein derartiges Verhalten legt React mit dem DOM an den Tag.

Werden im Gebäude mehrere Räume zugunsten eines Großraumbüros zusammengelegt, würde das Framework wie folgt damit umgehen: Erst identifiziert es alle stattfindenden Änderungen (Reconciliation) und anschließend aktualisiert es das DOM entsprechend. Wichtig ist, dass React kein neues Gebäude bauen, sondern nur die betroffenen Räume neu gestalten würde. Wird also ein DOM-Element verändert, sein Elternelement aber nicht, ist letzteres nicht betroffen.

Das React Starter Kit[1] gibt es als Download oder als JSFiddle Snipped zum direkten Loslegen. Clientseitig reicht es, die react.js-Datei einzubinden. Optional lässt sich noch JSXTransformer.js hinzufügen, um die in JavaScript XML (JSX) verfassten Komponenten parsen zu können (später dazu mehr). React-Code kann man anschließend in einem Script-Block mit type="text/js" entwickeln:

<!DOCTYPE html>
<html>
<head>
<script src="build/react.js"></script>
<script src="build/JSXTransformer.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/jsx">
React.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
</script>
</body>
</html>

Der JSX-Code steht nicht im Code der Website, sondern ist in eine separate Datei ausgelagert, mit Tools wie Webpack oder Browserify zusammengestellt und einmalig eingebunden.

Ist Webpack und das dazugehörige npm-Modul jsx-loader installiert, reicht die folgende Datei webpack.config.js aus, um direkt loszulegen zu können:

'use strict';

var path = require('path'),
targetPath = path.join(__dirname, 'build', 'assets'),
config;

config = {
resolve: {
extensions: [ '', '.js', '.jsx' ]
},
entry: [
'./site.jsx'
],
output: {
path: targetPath,
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.jsx$/, loader: 'jsx-loader' },
]
}
};

module.exports = config;

Das Bundle ist vor dem schließenden Body-Tag der HTML-Seite einzubinden:


...
<script type="text/javascript" src="build/assets/bundle.js"></script>
</body>

Aufbau, Syntax und Eigenschaften

Auf der Frontend-Seite besteht React aus Komponenten (Views):

React.renderComponent(
React.DOM.h1(null, 'Hello, world!'),
document.getElementById('example')
);

Mit JSX sieht das Ganze ähnlich aus:

/** @jsx React.DOM */
React.renderComponent(
<h1>Hello, world!</h1>,
document.getElementById('example')
);

Auch wenn das Mischen von JavaScript und HTML auf den ersten Blick etwas seltsam wirkt, schwindet das Gefühl nach kurzer Zeit. Das Trennen des Codes der beiden Sprachen ist im Grunde eine sogenannte Separation of Languages und das Zusammenführen widerspricht nicht dem Prinzip der Separation of Concerns.

Wenn eine Komponente zu groß wird, ist es sinnvoll, sie auf mehrere kleinere, wiederverwendbare Komponenten zu verteilen. Sie sind im Idealfall so klein, dass sie sich gut mit Unit-Tests abdecken lassen sowie eine klare und saubere Struktur der Anwendung ermöglichen.

Auf den ersten Blick sehen React-Komponenten genauso aus wie alltägliches HTML. Dem ist allerdings nicht so, denn es handelt sich lediglich um eine XML-ähnliche Darstellung, die das Entwickeln von JavaScript Renderings angenehmer machen soll.

Attributnamen wie class oder for sind dabei nicht erlaubt. Die Äquivalente className und htmlFor bieten deshalb die Funktionen des HTML-Originals. Diese und weitere Eigenarten der Syntax lassen sich in den JSX Gotchas[2] in der Dokumentation des Projekts nachlesen.

Browser können JSX-Code nicht ausführen, weshalb er in natives JavaScript zu transformieren ist. Ein Vorher-Nachher-Beispiel zeigt den Effekt:

var AComponent;
// Input (JSX):
var app = <AComponent text="Hello" />;
// Output (JS):
var app = React.createElement(AComponent, {text:"Hello"});

Komponenten lassen sich mit dem Befehl React.createClass erstellen. Ihnen ist eine Objektspezifikation zu übergeben, die neben vielen optionalen Methoden für das Verhalten mit der render-Methode eine einzige Pflichtangabe enthält:

var AComponent = React.createClass({
render: function(){
return (
<h1>Hello, world!</h1>
);
}
});

Anschließend kann man die Methode in ein beliebiges DOM-Element rendern:

React.renderComponent(
<AComponent/>,
document.getElementById('example')
);

Anwendungen, die komplett aus React gestrickt sind, werden diese Methode nur ein einziges Mal für die Hauptkomponente aufrufen. Alle restlichen Komponenten wie Header, Navigation, Footer und Content sind in der Hauptkomponente gekapselt.

Um etwas Anzeigelogik einzubauen, kann die Elternkomponente sogenannte props (Properties) weitergeben. In den Child-Komponenten stehen die Eigenschaften mit dem Aufruf von this.props zur Verfügung:

var DayComponent = React.createClass({
render: function(){
return (
<h1>Today is {this.props.day}!</h1>
);
}
});

React.renderComponent(
<DayComponent day="Sunday" />,
document.getElementById('example')
);

Während props unveränderlich (immutable) angelegt und für den Read-only-Zugriff gedacht sind, ermöglicht state Interaktionen. Er ist innerhalb der Komponente zu finden und lässt sich beispielsweise beim Klick auf einen Button oder bei der Rückgabe eines REST-Services mit this.setState() verändern. Verwendet die render-Methode ebenfalls den Status der Komponente, wird sie bei einer Änderung automatisch neu gerendert.

Beim Erzeugen einer Komponente holt sie sich ihren Zustand optional mit der getInitialState-Methode:

var DayComponent = React.createClass({
getInitialState: function(){
return {
day: "Tuesday"
}
},
render: function(){
return (
<h1>{this.state.day}</h1>
)
}
});

Lädt die Anwendung Daten via GET Request vom Server, werden sie in die Komponenten gespiegelt. Beispielsweise lässt sich mit jQuery ein asynchroner Request an den Server stellen und bei erfolgreicher Rückgabe direkt auf den state setzen:

var DayComponent = React.createClass({
...
getInitialState: function() {
return {data: []};
}
componentDidMount: function() {
$.get({
url: this.props.url,
dataType: 'json',
success: function(data) {
this.setState({data: data});
}.bind(this);
});
}
...
});

Nach dem Rendern ruft React automatisch die componentDidMount-Methode auf. Aktualisiert die Anwendung den Status mit this.setState(), ersetzen die neuen Daten vom Server den anfänglichen data-Array und bringen die Komponente so auf den neusten Stand. Neben der componentDidMount gibt es noch weitere Methoden, die im nächsten Abschnitt genauer erklärt werden.

Objektspezifikation, Events und Datenfluss

Komponenten lassen sich mit einer Objektspezifikation individuell gestalten. Neben den bereits im Artikel verwendeten Angaben wie render gibt es noch weitere, optionale Methoden:


Für den Komponenten-Lifecycle:

Darüber hinaus gibt es noch weitere Methoden, zu denen sich detailliertere Informationen in der React-Component-Specs-Dokumentation[3] finden lassen.

In React sind Event Handler als Instanzen von SyntheticEvent zu übergeben. Der Cross-Browser-Wrapper stellt sicher, dass die Events in allen Browsern identisch funktionieren. Dabei bleibt die Schnittstelle den nativen Events (beispielsweise stopPropagation() und preventDefault()) treu.

Die Events werden in den React-Komponenten den Elementen als Attribute hinzugefügt und lösen eine in der Objektspezifikation definierte Methode aus, sobald sie eintreten:

var DayComponent = React.createClass({
getInitialState: function(){
return {
day: "Monday"
}
},
setSunday: function() {
this.setState({
day: "Sunday"
});
},
render: function(){
return (
<h1>{this.state.day}</h1>
<button onClick={this.setSunday}>Set sunday</button>
)
}
});

Der Datenfluss ist einer der einfachsten und gleichzeitig interessantesten Teile von React. Während bei Frameworks wie AngularJS von einem Two-way Binding die Rede ist, fließen die Daten bei React nur in die Richtung vom sogenannten Owner zum Child, wie es im Von-Neumann-Modell[4] beschrieben ist. Die Elternkomponente gibt den Status der ganzen untergeordneten Kette von Komponenten mit this.props weiter.

Sollen Daten in mehreren Komponenten zum Einsatz kommen, bindet man die Datenquelle häufig an eine zentrale Komponente und reicht sie den Unterelementen weiter.

var DayComponent = React.createClass({
getInitialState: function(){
return {
initialItems: [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
]
}
},
componentWillMount: function(){
this.setState({items: this.state.initialItems})
},
render: function(){
return (
<div>
<DayList items={this.state.items}/>
</div>
);
}
});

var DayList = React.createClass({
render: function(){
return (
<ul>
{
this.props.items.map(function (item) {
return <li>{item}</li>
})
}
</ul>
)
}
});

React.renderComponent(<DayComponent/>,
document.getElementById('example'));

Werden Formularfelder mit einem value-Attribut verwendet, ist von Controlled Components die Rede.

render: function() {
return <input type="text" value="Hallo!" />;
}

Dabei hat der User keine Möglichkeit, den Wert durch Eingaben zu verändern, da React ihn gesetzt hat. Soll er modifiziert werden, ist am entsprechenden State anzusetzen:

getInitialState: function() {
return {value: 'Hallo!'};
},
handleChange: function(event) {
this.setState({value: event.target.value});
},
render: function() {
var value = this.state.value;
return <input type="text" value={value} onChange={this.handleChange} />;
}

Durch das onChange-Event wird der State aktualisiert und entspricht dann der Eingabe des Users. Das Vorgehen lässt sich mit dem Two-Way Binding Helper[5] noch etwas vereinfachen.

Alternativ und ohne Angabe von value behandelt React die Felder als Uncontrolled Components. Die Angabe eines initialen Wertes ist aber nach wie vor mit defaultValue möglich:

render: function() {
return <input type="text" defaultValue="Hello!" />;
}

React und Flux

Auch wenn Entwickler React oft mit Flux kombinieren, ist Flux keine Erweiterung oder Bibliothek für React, sondern eine Architektur, die Facebook für clientseitige Webapplikationen verwendet. Sie ergänzt die View-Komponenten aus React mit einem unidirektionalen Datenfluss.

Eine Flux-Applikation besteht aus Actions, Stores, Views (den React-Komponenten) und einem zentralen Dispatcher, wie Abbildung 1 zeigt.

Die Flux-Architektur umfasst grob vier Elemente und dient der Entwicklung clientseitiger Webanwendungen (Abb. 1).
Die Flux-Architektur umfasst grob vier Elemente und dient der Entwicklung clientseitiger Webanwendungen (Abb. 1).


Das ganze Prinzip ähnelt einem klassischen MVC-Pattern, unterscheidet sich aber im Datenfluss: Wenn ein Benutzer mit einer React View interagieren möchte oder eine Datenquelle wie eine REST API angesprochen wird, erhält der zentrale Dispatcher eine entsprechende Action. Er verteilt sie anschließend auf die einzelnen Stores (Business-Logik), die wiederum alle betroffenen Views aktualisieren.

Einen Weg zurück gibt es nicht, die View-Layer darf also nicht den state direkt ändern, sondern muss eine Fire-and-forget-Anweisung an den zentralen Dispatcher senden. Dieser wiederum leitet sie weiter und folgt dem Kreis bis zur betroffenen View. Dadurch haben Views nur die Verantwortung, den aktuellen state der Anwendung zu rendern.

Es gibt viele Fälle, in denen solch ein Szenario genau passt – das von Facebooks sozialem Netz ist besonders bekannt. Erhält ein Benutzer dort eine neue Nachricht, so passieren zwei Sachen:

Klickt der Benutzer nun ins Chat-Fenster, so verschwindet das Hinweis-Icon im Menü.

Dies in einer klassischem MVC-Anwendung abzubilden, würde möglicherweise bedeuten, dass das Nachrichten-Model und das Nicht-gelesene-Nachrichten-Model zu aktualisieren sind. Dadurch kann eine nicht gewollte Abhängigkeit zwischen den Models entstehen. Situationen, in denen sich mehrere Teile einer Webseite anpassen, sind keine Seltenheit – und diese kaskadierenden Änderungen führen oft zu unnötig komplexen Datenströmen.

In Flux würde der Klick ins Chat-Fenster mit einer neuen Action den zentralen Dispatcher benachrichtigen. Die darauf registrierten Stores entscheiden dann eigenständig, was sie mit dem Update machen (wie das Hinweis-Icon auszublenden oder die Nachricht einfach zu ignorieren). Dafür spricht auch eine klarere "Separation of Concerns", denn keine Komponente außerhalb des Stores weiß, wie letzterer intern die Daten seiner Domäne verwaltet.

Den gesamten Datenfluss einer Flux-Applikation steuert der zentrale Dispatcher. Er soll auf komplexe Logik verzichten und erhält Actions aus unterschiedlichen Quellen (z.B. Benutzerinteraktionen). Die Actions werden auf die einzelnen Stores verteilt, die sich zuvor mit einem Callback registriert hatten.

Der Dispatcher verteilt die Actions an alle registrierten Stores (Abb. 2).
Der Dispatcher verteilt die Actions an alle registrierten Stores (Abb. 2).


Indem der Dispatcher die registrierten Callbacks in einer bestimmten Reihenfolge ausführt, verwaltet er Abhängigkeiten zwischen den einzelnen Stores:

var Dispatcher = require('flux').Dispatcher;
var AppDispatcher = new Dispatcher();

AppDispatcher.handleViewAction = function(action) {
this.dispatch({
source: 'VIEW_ACTION',
action: action
});
}

module.exports = AppDispatcher;

Die dispatch-Methode gibt den Action-Payload an alle registrierten Callbacks weiter. Angaben wie VIEW_ACTION helfen dabei, die Events von View und Server/API zu unterscheiden.

Verglichen mit einem Model in der klassischen MVC-Welt, verwaltet ein Store nicht nur ein Model, wie es bei ORM-Models der Fall ist, sondern den Status mehrerer Objekte. Damit ist der Application State für eine bestimmte Domäne innerhalb der Anwendung gemeint.

Um neue Nachrichten zu empfangen, registriert sich ein Store mit einem Callback am zentralen Dispatcher. Der Callback enthält die Action als Parameter. Durch den Typ der Action lässt sich erkennen, welche interne Methode als Nächstes auszuführen ist:

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var CHANGE_EVENT = 'change';

var _todos = {};

var TodoStore = assign({}, EventEmitter.prototype, {

getAll: function() {
return _todos;
},

emitChange: function() {
this.emit(CHANGE_EVENT);
},

addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},

removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
}

AppDispatcher.register(function(action) {
var text;

switch(action.actionType) {
case 'LOAD_TODOS':
// Call internal method like loadTodos...
_todos = action.data;
TodoStore.emitChange();
break;

// ...
}
});

module.exports = TodoStore;

Das Beispiel zeigt, dass der Store die EventEmitter von NodeJS erweitert, sodass Stores auf Events hören oder selbst welche senden können. Der Store löst bei einer Änderung ein Event aus, mit dem sich die View wiederum aktualisieren kann.

Die React-View-Komponente kann sich im addChangeListener des Stores registrieren, um bei Änderungen benachrichtigt zu werden:

var TodoStore = require('../stores/TodoStore');

function getTodoState() {
return {
allTodos: TodoStore.getAll()
};
}

var TodoApp = React.createClass({


componentDidMount: function() {
TodoStore.addChangeListener(function() {
this.setState(getTodoState());
});
}
...
}

Aktionen und Fazit

In einer React-App läuft ein Großteil der Interaktionen über Actions. Action Creators sind semantische Hilfsmethoden, die die Actions zum zentralen Dispatcher senden. Sie lassen sich in den Event Handlers der Views oder bei Antwort vom Server ausführen.

In einer Flux-Anwendung bewegt sich der gesamte Datenflow nur in eine Richtung (Abb. 3).
In einer Flux-Anwendung bewegt sich der gesamte Datenflow nur in eine Richtung (Abb. 3).

var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');
var TodoActions = {
updateText: function(id, text) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_UPDATE_TEXT,
id: id,
text: text
});
}
};
module.exports = TodoActions;


Während es vor ein paar Jahren noch kaum denkbar war, eine komplette Anwendung in JavaScript zu entwickeln, migrieren heute viele Firmen ihre Webseiten auf flexible Frameworks wie React. Das isomorphe Projekt lässt sich auf dem Server ausführen und erhöht dadurch die Performance im Browser. Zugleich ergibt sich so für suchmaschinenrelevante Seiten ein angenehmer Vorteil, und die Möglichkeit, Code sowohl auf Client- als auch auf Serverseite zu nutzen, spart eine Menge Entwicklungsaufwand.

Durch das virtuelle DOM gestalten sich Unit-Tests der Anwendung ebenfalls einfacher, denn mit der Übergabe von state in den einzelnen Komponenten lässt sich das generierte HTML prüfen. Mehr zum Thema Testen und ein tieferer Einblick in diverse Flux-Implementierungen gibt es im zweiten Teil des Artikels – "React in der Praxis". (jul[6])

Roberto Bez[7]
ist passionierter Webentwickler und begeistert von neuen Technologien, die er versucht, in die tägliche Anwendungsentwicklung einzubringen.


URL dieses Artikels:
http://www.heise.de/-2689175

Links in diesem Artikel:
[1] https://facebook.github.io/react/docs/getting-started.html
[2] http://facebook.github.io/react/docs/jsx-gotchas.html
[3] http://facebook.github.io/react/docs/component-specs.html
[4] http://en.wikipedia.org/wiki/Von_Neumann_architecture
[5] http://facebook.github.io/react/docs/two-way-binding-helpers.html
[6] mailto:jul@heise.de
[7] http://devangelist.de/