9elements hat viele Single Page Applikationen (kurz SPA) mit Backbone entwickelt z.B. das noch in der Beta befindliche OptimusTime. Backbone ist ein relativ schlankes Framework und sein Autor Jeremy Ashkenas hat sich auch explizit dagegen ausgesprochen, das Framework mit mehr Features leistungsfähiger zu machen. Da wir gesehen haben, dass man viele Dinge in Backbone immer wieder auf die gleiche Art und Weise programmieren muss, haben wir Chaplin entwickelt. Chaplin abstrahiert z.B. CollectionViews oder stellt einem über den Mediator einen Event-Bus zur Verfügung. Doch auch Chaplin hat inzwischen ein paar Jahre auf dem Buckel und es gibt moderne Frameworks wie z.B. Angular oder Ember mit weit mehr Features (wie z.B. Two-Way Data Binding).
Wenn man seine SPA allerdings auf Angular oder Ember umschreiben möchte, kommt das einem kompletten Rewrite gleich. Aus produkttechnischer und ökonomischer Sicht ist ein kompletter Rewrite eine ziemlich gefährliche Angelegenheit. Oft bleiben Features auf der Strecke, oder es werden Bugs eingeführt, so dass das Produkt gerne bei der Gelegenheit zur völligen Unbenutzbarkeit “verbessert” wird.
Facebook hat die Bibliothek React.js veröffentlicht. Im Gegensatz zu einem Framework, welches sich clientseitig um den kompletten Stack kümmert, bildet React.js lediglich den View-Teil ab. Man könnte React.js am ehesten mit den Directives von Angular.js vergleichen. Ziel dieses Blogposts ist das Refactoring einer Backbone / Chaplin View mittels React.js.
Refactoring der Backbone Views mit React.js
Um eine typische Backbone View durch React zu ersetzen ist der einfachste Weg, die render Methode zu überschreiben
und dort explizit die React-Komponente in this.el zu rendern. Etwaige Parameter werden als Objekt übergeben.
var IndexView = React.createFactory(IndexViewComponent);
var TimerView = Backbone.View.extend({
...
render: function() {
React.render(IndexView({
collection: collection,
projects: mediator.projects.models,
tags: mediator.tags.models,
editTrackingClicked: editTrackingClicked.bind(this),
resumeTrackingClicked: resumeTrackingClicked.bind(this),
splitTrackingClicked: splitTrackingClicked.bind(this),
changeProjectForTracking: changeProjectForTracking.bind(this),
changeDescriptionForTracking: changeDescriptionForTracking.bind(this)
}), this.el);
}
});
Der Vorteil ist, dass wir ab dann direkt mit React-Komponten weiterarbeiten können. Ein wichtiger Hinweis: Es ist sinnvoll React-Komponenten anhand einer angemessenen Hierarchie zu verschachteln. Zusätzlich nutzen wir ein Mixin bei GitHub, welches die render Methode bei Veränderung des Models oder Collection erneut aufruft.
Folgendermaßen kann das Mixin mit einem Model benutzt werden…
var MyComponent = React.createClass({
mixins: [Backbone.React.Component.mixin],
render: function () {
return <div>{this.state.model.foo}</div>;
}
});
var model = new Backbone.Model({foo: 'bar'});
React.render(<MyComponent model={model} />, document.body);
// Update the UI
model.set('foo', 'Hello world!');
… und so mit einer Collection:
var MyComponent = React.createClass({
mixins: [Backbone.React.Component.mixin],
createEntry: function (entry) {
return <div key={entry.id}>{entry.helloWorld}</div>;
},
render: function () {
return <div>{this.state.collection.map(this.createEntry)}</div>;
}
});
var collection = new Backbone.Collection([
{id: 0, helloWorld: 'Hello world!'},
{id: 1, helloWorld: 'Hello world!'}
]);
React.render(<MyComponent collection={collection} />, document.body);
Vorteile
Wer mit komplexeren Views bei Backbone arbeitet, wird die Problematik kennen, dass man Events von Subviews selbst managen muss. Ein Beispiel: Bei unserem TimeTracking gibt es die TrackingView, auf der viele Trackings in einer Liste angezeigt werden.
Jedes Tracking kann mit dem Button “Split” in den SplitView-Modus geschaltet werden. Im SplitView-Modus wird aus dem Tracking ein Scheren-Widget, welches interaktiv den Aufteilungspunkt für ein neues Tracking visualisiert.
In der alten Backbone-View musste dafür eine Subview erstellt werden. Auf dieser Subview mussten dann dynamisch viele Mouse-Events aufgefangen werden. Beim Verlassen der SplitView mussten diese Events wieder sauber abgeräumt werden.
Das ist etwas, bei dem React richtig glänzen kann. Der komplette Code für das Splitting wurde aus der Komponente extrahiert und in eine eigene React-Komponente ausgelagert, die so benutzt werden kann:
<CircleSplitView
onClick={this.splitOverlayClicked}
onChange={this.splitOverlayChanged}
/>
Diese Komponte wird nur angezeigt, wenn sich das Tracking im Splitmodus befindet. Während wir mit Backbone/Chaplin uns selbst um den Aufbau des DOMs und das Anhängen der Events kümmern mussten, wird diese Aufgabe nun von React übernommen.
Nachteile
Wenn man anfängt, komplexere Komponentenarchitekturen zu bauen, dann kann die Event-Delegation mitunter sehr komplex werden. Ziel sollte es sein, dass Daten erst in der Backbone / Chaplin-Controllerschicht verändert werden. Angenommen ein Edit-Input ist unter mehreren Schichten von React-Komponenten vergraben, dann hat man zwei Möglichkeiten die Veränderung der Daten zu realisieren:
Delegationskette
Die erste Möglichkeit besteht darin, die Delegate-Funktion explizit als Parameter zu übergeben:
<Haupkomponenten onChange={this.onChange}>
<SubKomponente1 onChange={this.props.onChange}>
<SubKomponente2 onChange={this.props.onChange}>
<SubKomponente3 onChange={this.props.onChange}>
<EditInput onChange={this.props.onChange}>
Mediator-Pattern
Eine andere Möglichkeit besteht darin, via Publish / Subscribe ein Event zu erzeugen und mittels Event-Bus zu propagieren…
function onChange(payload) {
Mediator.publish('onChange', payload);
}
…und dann an der entsprechenden Stelle zu fangen und zu verarbeiten:
this.listenTo(Mediator, 'onChange', this.onChange);
Das Mediator-Pattern dient also der Entkopplung - bringt allerdings eine schwierigere Testbarkeit mit sich.
Fazit
Wir haben dieses Refactoring bei OptimusTime erfolgreich durchgeführt und konnten den Code stellenweise drastisch vereinfachen. Viele Bugs die sich aus z.B. dem sauberen Abräumen von Events ergeben haben gehören nun der Vergangenheit an. Wenn euch dieser Artikel gefallen hat, dann folgt uns doch auf Twitter. Wie hat euch React.js geholfen euern Code verbessern - wir freuen auf eine Dikussion in den Kommentaren.
Sebastian Deutsch
Gründer von 9elements.com
Sebastian Deutsch ist Gründer und Geschäftsführer von 9elements, einer Softwareagentur mit Sitz in Bochum, die sich auf hochwertige Web-Applikationen und kreative Technologie spezialisiert hat. Er arbeitet mit React seit dem initialen Release der Bibliothek im Jahr 2013 und gehört damit zu den erfah… Mehr zu Sebastian Deutsch
Weitere Artikel
React Labs: View Transitions, Activity und weitere Features in Entwicklung
Ein tiefer Einblick in die neuesten experimentellen Features von React: View Transitions für flüssige Animationen, die Activity-Komponente für bessere State-Verwaltung und mehr. Erfahre, was die Zukunft von React bereithält.
React 19.2: Die neuesten Features für Anfänger erklärt
React 19.2 bringt spannende neue Features wie den Activity Component, useEffectEvent Hook, Performance-Tracking in Chrome DevTools und verbesserte Server-Rendering-Funktionen. Erfahre alles über die neuen Möglichkeiten.
React Foundation angekündigt: Eine neue Ära für das React-Ökosystem
Meta gibt React und React Native an die neu gegründete React Foundation ab. Erfahre alles über die revolutionären Änderungen, die sieben Gründungsmitglieder und was das für die Zukunft der React-Entwicklung bedeutet.