logo_og1

UIs mit Facebooks React

Von am 26.07.2014

Für JavaScript gibt es mittlerweile viele Bibliotheken, um graphische Benutzeroberflächen zu erzeugen. Obwohl diese recht unterschiedlich sein können, haben sie jedoch eines gemeinsam und zwar wie sie mit Änderungen umgehen. In diesem Artikel gehen wir darauf ein, was Facebook mit ihrem eigener UI-Bibliothek anders macht.

Zu Beginn ein simples Beispiel. Nehmen wir an, wir hätten eine Applikation geschrieben, welche Kommentare (oder Nachrichten, Produkt-Reviews, usw.) anzeigt und die Möglichkeit bietet, selbst Kommentare zu verfassen. Werden Kommentare von jemandem (man selbst oder jemand an einem ganz andere Browser/Computer) gepostet, werden diese sofort angezeigt. Das heißt also, dass wir eine aktive Verbindung (Polling, WebSocket, REST, usw.) zu einem Server haben, von welchem wir ständig die neuesten Kommentare bekommen.

Zwei traditionelle Herangehensweisen an das Realisieren dieser Applikation könnten so aussehen:
1) Eine Funktion die zu Beginn die Liste der Kommentare erstellt und eine welche die Updates in die vorhandene Liste einfügt. Dies hat den Nachteil, dass wir zwei Funktionen brauchen die fast das Gleiche machen, dies aber schnell (Vorteil). Zusätzlich ist es nicht besonders data-driven und sehr imperativ, da die Liste ständig manipuliert wird und ein Eigenleben durch Hidden-State entwickeln kann. template(model) != view. Eine weitere Überlegung: Was ist wenn wir im Nachhinein beschließen, dass Kommentare sowohl veränderbar und löschbar sind, natürlich von jedem Browser aus und dass soll jede andere Person sofort in ihren Browsern sehen können. Dies würde wieder zwei weitere Funktionen benötigen, die darüber Bescheid wissen müssen, welches Kommentar die Veränderung und Löschung betrifft (weiterer State 🙁 ). Da können wir froh sein, dass JS single-threaded ist, denn in anderen Programmiersprachen wären wir nun in der Locking-Hölle (4 Funktionen die auf denselben State zugreifen – shared State).
2) Eine Funktion welche die bestehende Liste komplett entfernt und zusammen mit allen vorherigen Updates neu aufbaut. Nachteil: schlechte Geschwindigkeit/Performance | Vorteil: data-driven, functional, template(model) == view, kein Hidden-State

Facebook hat erkannt, dass man mit keiner der beiden Lösungen glücklich wird. Dies liegt der Entstehung von React zugrunde. Dabei ist React aber kein Framework, es zwingt den Nutzern nichts, wie zum Beispiel Routers oder Templates, auf. Man muss bestehende Applikationen nicht komplett auf React umstellen, sondern kann es für einzelne Komponenten verwenden und es spielt dabei gut mit anderen Bibliotheken wie jQuery zusammen.
Die Herangehensweise von React zu unserem vorherigen Beispiel ist es, eine Funktion zu erstellen, welche für eine gegebene Liste von Kommentaren das HTML erzeugt, also die Funktion von 2). Diese ruft React selbstständig auf, falls sich die Liste von Kommentaren verändert, wenn wir zum Beispiel an das Kommentaren-Array die Updates anhängen. Dabei greifen wir das HTML nicht direkt an, sondern ändern nur ein JavaScript-Array. React produziert nun selbstständig das HTML und vereint es mit dem was schon im DOM vorhanden ist. Daher löscht es nicht das gesamte DOM, sondern fügt nur die Differenz ein. Mit dieser Herangehensweise haben wir alle Vorteile der beiden anderen. Wir bekommen ein functional, data-driven Interface ( template(model) == view ), behalten aber die Geschwindigkeit der imperativen Lösung 1) durch das Vereinen von Differenzen mit dem was schon am Bildschirm vorhanden ist.

Ganz konkret sieht das in React dann so aus:
Variante 1 mit Properties:

/** @jsx React.DOM */
var Kommentare = React.createClass({
    render: function() { return (
        <ol>
            {this.props.kommentare.map(function (kommentar) {
                return (<li>{kommentar}</li>);
            })}
        </ol>
  );}
});

var kommentare = ["Kommentar 1", "Kommentar 2"];

setInterval(function() {  // "Server-Verbindung"
  var neueKommentare = ["Kommentar 3"]; // vom Server
  kommentare = kommentare.concat(neueKommentare);
  React.renderComponent(
    <Kommentare kommentare={kommentare} />,
    document.body
  );
}, 50);

Variante 2 mit State:

/** @jsx React.DOM */
var Kommentare = React.createClass({
    getInitialState: function() {
        return {kommentare: []};
    },
    componentDidMount: function() {
        setInterval(function() { // "Server-Verbindung"
            var neues_array = this.state.kommentare.concat(neue_kommentare);
            this.setState({kommentare: neues_array});
        }, 50);
    },
    render: function() { return (
        <ol>
            {this.state.kommentare.map(function (kommentar) {
                return (<li>{kommentar}</li>);
            })}
        </ol>
  );}
});

React.renderComponent(
  <Kommentare />,
  document.body
);

Der Grundbaustein von React ist der Component (React.createClass), welcher definiert, wie das resultierende DOM aussehen soll. React wird mit einer optionalen domain-specific-language ausgeliefert, namens JSX (hier das XML-Markup im JavaScript). Das JSX wird vor dem Ausliefern zu JavaScript kompiliert. Hat man größere Projekte steigert JSX die Leserlichkeit ungemein und kann ich daher nur empfehlen. Wer lieber pures JS schreiben will, muss Elemente mit zum Beispiel

React.DOM.div({})

erzeugen, was natürlich weitaus schwieriger zu lesen ist als

<div></div>

.
Als React-Umsetzung unseres Beispiels sind zwei gleichwertige Varianten angeführt. Ein setInterval simuliert in beiden Varianten die Server-Verbindung. Variante 1 verlässt sich dabei nur auf Daten von außen (kein Hidden-State). Jedes Mal, wenn wir neue Kommentare vom Server erhalten, hängen wir diese an unser Kommentaren-Array an und lassen React die Liste an Kommentare neu rendern (React.renderComponent-Funktion). Wie schon zuvor erwähnt, wird nicht die gesamte ol-Liste neu gerendert, es werden nur Differenzen zwischen neuem und alten State appliziert. In Variante 2 definieren wir einen Component, der sich selbst um seine Server-Verbindung kümmert und sich selbst neu rendert (mit jedem Aufruf von this.setState()). Es bleibt dem Programmierer überlassen, welche Variante bevorzugt wird. Meine Empfehlung geht an Variante 1, da kein Hidden-State verwendet wird.
Wie in den Varianten gezeigt wird, bedarf es nur einer Funktion (render), welche definiert wie das Component zu jedem Zeitpunkt auszusehen hat. Wir kümmern uns danach als Programmierer nur mehr um das Manipulieren von Daten (Arrays, Objecte, …) und nicht um das Manipulieren des DOMs (createElement, getElementBy…, appendChild, … fällt alles weg 🙂 ). Das macht React für uns. Hiermit können wir besseren Code schreiben, da wir nicht ständig Funktionen wie appendChild, innerHTML und der Gleichen aufrufen müssen und können unsere ganze Aufmerksamkeit unserem Model widmen.

1 Kommentar

The comments are closed.