JavaScript: EinfĂĽhrung in React

Seite 4: React und Flux

Inhaltsverzeichnis

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).


  • Actions: Hilfsmethoden, um Daten an den Dispatcher zu transportieren. Beispiel: updateText(todoId, newText). Actions können aus verschiedenen Quellen wie Benutzerinteraktionen (Klick auf einen Button) oder neuen Antworten vom Server entstehen.
  • Dispatcher: erhält Actions und sendet die Payload zu den registrierten Callbacks (Stores). Der Dispatcher ist eine zentrale Instanz in der Anwendung und steuert somit den gesamten Datenfluss.
  • Stores: enthalten den Application State und die Logik und haben Callbacks auf den Dispatcher registriert. Beim Vergleich mit klassischen Patterns wie Model View Controller wĂĽrden sie die Models darstellen.
  • Controller Views: React-Komponenten, die sich den state von den Stores holen und ihn den Child-Komponenten als props weiterreichen. Sie kĂĽmmern sich um das Rendering der Anwendung.

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:

  • Die Nachricht erscheint im Chat-Fenster.
  • In der MenĂĽ-Leiste weist ein rotes Hinweis-Icon auf eine neue Nachricht hin.

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).


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());
});
}
...
}