9 Min. Lesezeit

Im Rahmen dieses Blogs möchte ich dir zeigen, wie du das State-Handling von Webapplikationen im Allgemeinen und von Angular Apps im Speziellen, mithilfe von Redux und ReactiveX, in den Griff bekommst. Dabei werde ich dir bekannte Probleme, grundlegende Konzepte und konkrete Technologien vorstellen und mit kleinen Beispielen untermauern. Zu guter letzt kannst du dir ein lauffähiges Beispiel herunterladen.

State-Handling von Webapplikationen

Innerhalb einer Webapplikation ist der State im Prinzip der Zustand der Applikation, zu einem bestimmten Zeitpunkt. Der Zustand lässt sich für gewöhnlich aus den gesetzten Variablenwerten und Events ableiten.

Doch wo wird dieser Zustand gespeichert, und wie hältst du bestimmte Informationen innerhalb einer Webapplikation synchron?

Diese Probleme sind bei einer stark wachsenden Codebasis nicht trivial. Je größer die Applikation wird, desto mehr Stellen musst du synchron halten.

Früher musstest du dich als Entwickler selbst darum kümmern, bestimmte Informationen über die Anwendung hinweg zu synchronisieren. Der aktuelle Applikationszustand war über unzählige Komponenten verteilt. Das hat eine einfache Synchronisierung erschwert. Einzelne Komponenten einer Webapplikation mussten sich gegenseitig über geänderte Zustände informieren und aktualisieren (siehe Abbildung 1).

State-Handling einer Webapplikation mit und ohne Redux
Abbildung 1: State-Handling einer Webapplikation mit und ohne Redux

 

Heute gibt es für diese Art von Problemen Lösungen. Zum Beispiel den zentralen Redux-Store.

Zentraler State mit Redux-Store

Redux ist ein State-Container für JavaScript-Applikationen. Bitte nicht mit Flux verwechseln. Flux ist lediglich ein Pattern und keine konkrete Bibliothek. Außerdem gibt es bei Flux ein paar grundsätzliche Unterschiede zu Redux (siehe Flux versus Redux).

Im Folgenden findest du die Vorteile von Redux, gegenüber herkömmlichen Ansätzen.

Vorhersagbar

Redux hilft dir, Applikationen zu schreiben, die sich konsistent verhalten. Sie können in verschiedenen Umgebungen (Client, Server) ausgeführt werden und sind einfach zu testen.

Zentralisiert

Die Zustände werden nicht über zahlreiche Komponenten hinweg in der Applikation gehalten, sondern zentral an einer einzigen Stelle: im sogenannten Redux-Store.

Das bringt sehr viele Vorteile mit sich. Undo- und Redo-Funktionalitäten oder das Handling von Zustandsänderungen kann ich beispielhaft nennen.

Beim Einsatz sehr vieler UI-Komponenten brauchen sich diese nicht mehr untereinander über Änderungen zu informieren, weil sie direkt und automatisch durch den zentralen Redux-Store über Änderungen in Kenntnis gesetzt werden.

Debugfähig

Mit den Redux-DevTools kann man leicht verfolgen, wann, wo, warum und wie sich der Zustand der Applikation geändert hat. Mit der Redux-Architektur kann man Änderungen protokollieren, Time-Travel-Debugging verwenden und sogar vollständige Fehlerberichte an einen Server senden.

Flexibel

Redux kannst du mit beliebigen Frameworks wie Angular, React oder Vue.js kombinieren. Damit verfügst du über ein großes Ökosystem an Addons, das auf deine Bedürfnisse zugeschnitten ist.

Redux-Architektur

Für gewöhnlich interagiert ein Nutzer mit der Oberfläche und löst dadurch an irgendeiner Stelle ein bestimmtes Event aus (siehe Abbildung 2). Dieses Event wiederum löst in einer Redux-Architektur eine sogenannte Action aus. Diese Action besitzt einen definierten Typ und ein Objekt, das die Zustandsänderung beinhaltet.

Wichtig ist nun, dass es in der Redux-Architektur nie zu einer direkten Zustandsänderung kommt, denn der zentrale Redux-Store ist unveränderlich.

Es werden immer nur neue Zustände mithilfe sogenannter Reducer-Functions basierend auf dem alten Zustand für den Store erzeugt. Es ist nicht möglich, den Store direkt zu manipulieren. Diese strikte Trennung und der Einsatz einfacher Pure Functions für die Reducer vereinfachen auch das Testen der Applikation.

Redux-Architektur
Abbildung 2: Redux Architektur

 

Asynchrone Programmierung mit ReactiveX

Da Zustandsänderungen während der Interaktion mit der Oberfläche zu jeder Zeit auftreten können und du nicht jedes Mal die ganze Webapplikation neu laden möchtest, hast du als Entwickler zwangsläufig auch mit asynchronen Requests und reaktiver Programmierung zu tun.

ReactiveX ist eine API für asynchrone Programmierung mit Observable Streams. Observables bieten Unterstützung für die Übermittlung von Nachrichten zwischen Publishern und Subscribern in deiner Applikation.

Observables bieten gegenüber anderen Verfahren erhebliche Vorteile für die Ereignisbehandlung, für die asynchrone Programmierung und für die Verarbeitung mehrerer Werte (siehe Codebeispiel 1), mithilfe sogenannter Pure Functions.

const squareOdd = of(1, 2, 3, 4, 5)
  .pipe(
    filter(n => n % 2 !== 0),
    map(n => n * n)
  );

// Subscribe to get values
squareOdd.subscribe(x => console.log(x));
// Output: 1925

Codebeispiel 1: ReactiveX-Beispiel Publisher/Subscriber

 

Observables sind deklarativ. Das bedeutet, sie definieren eine Funktion zum Veröffentlichen von Werten, die jedoch erst ausgeführt wird, wenn ein Consumer sie abonniert. Der Consumer erhält dann Benachrichtigungen, bis die Funktion abgeschlossen ist oder er sich abmeldet.

ReactiveX ist eine Kombination der besten Ideen

  • des Observer-Patterns,
  • des Iterator-Patterns und
  • der funktionalen Programmierung.

Warum sollte man ReactiveX nutzen?

Mit ReactiveX ist es sehr leicht, Event-Streams oder Data-Streams zu erzeugen. Zudem ist es leicht möglich, Streams mit einer Vielzahl von Operatoren zu komponieren oder zu transformieren.

ReactiveX hilft dir auch, Seiteneffekte innerhalb der Applikation zu erzeugen, indem man als Consumer Observables abonniert und das gewünschte Verhalten implementiert.

ReactiveX ist erst einmal nur eine API und damit unabhängig von der Plattform.

Die Einsatzgebiete gehen weit über die normale Webentwicklung hinaus.

  • Web mit RxJS, oder Mobile mit Rx.NET und RxJava
  • Java, Scala, C#, C++, Clojure, JavaScript, Python, Groovy, JRuby, und andere

RxJS für das Web

Die Reactive Extensions Library for JavaScript implementiert die ReactiveX API und dient der Entwicklung von Applikationen basierend auf JavaScript.

Der Einsatz von Pure Functions zur Transformation von Werten schafft zum einen Klarheit über den Programmablauf und vermeidet zum anderen Fehler und ungewollte Seiteneffekte. Um Funktionen zu verketten, bietet RxJS die Funktion pipe() (siehe Codebeispiel 1). Als Parameter kann man dabei die unterschiedlichen Pure Functions übergeben, zum Beispiel filter oder map.

Mithilfe von RxJS können asynchrone Programmabläufe besonders elegant beschrieben werden, ohne dabei eigene Funktionen implementieren zu müssen. Ein großer Fundus an Pure Functions versetzt den Entwickler in die Lage, komplexe Programmabläufe zu komponieren beziehungsweise zu transformieren.

Redux-Store im Kontext von Angular

Für Angular-Webapplikationen gibt es aktuell zwei populäre Bibliotheken, um einen Redux-Store in die Anwendungsarchitektur zu integrieren.

Zum Einen ist dies der Angular Redux Store (@angular-redux/store), der die Webapplikation bzw. Angular um praktische Redux-Store-Bindings erweitert.

Zum Anderen ist dies der NgRx Store, der sehr ähnlich funktioniert und lediglich im Bereich der Isolierung von Seiteneffekten ein paar formale Unterschiede zum @angular-redux/store aufweist.

@angular-redux/store

Mithilfe der Bindings des Angular Redux Stores hat man innerhalb der Angular-Komponenten einen sehr intuitiven und praktischen Zugriff auf den zentralen Redux-Store.

Das ist in folgendem Codebeispiel zu sehen:

@Injectable()
export class TimelineStateService {

 @select((state: AppState) => state.timeline.events)
 public events$: Observable<IEvent[]>;

 @dispatch()
 public loadTimeline(id: string) {
   return {type: ‘LOAD_TIMELINE_REQUEST’, payload: id};
 }
}

Codebeispiel 2: Zustände auslesen und Aktionen auslösen mit dem @angular-redux/store

 

Mit der Annotation @select() kannst du den Redux-Store direkt auslesen.

Ändert sich der Applikationszustand, bekommt der Consumer (hier: events$) eine Nachricht mit dem aktualisierten Zustand zugeschickt.

Mit @dispatch() hingegen annotiert man eine Funktion, die eine Action, bestehend aus einem Type und einer Payload-Property, direkt an die Reducer sendet.

@select(state => state.counter * 2)
selectMultipliedByTwo: Observable<number>;

Codebeispiel 3: Auslesen und direkte Transformation eines Zustands mit dem @angular-redux/store

 

Darüber hinaus ist es auch möglich, die ausgelesenen Werte vor ihrer Verwendung zu manipulieren (siehe Codebeispiel 3). Alternativ dazu gibt es noch mehr Möglichkeiten, Werte aus dem Redux-Store über eine Komponente auszulesen und zu transformieren.

Mit einem solchen Store kann man beliebig viele Komponenten bei einer Zustandsänderung automatisch synchronisieren, ohne sich über Inkonsistenzen Sorgen machen zu müssen.

 

Angular Redux Store Beispiel herunterladen

 

Zusammenfassung

Bei größeren Webapplikationen ist die Verwaltung des Applikationszustands nicht trivial.

Die Redux-Architektur löst dieses Problem mit ihrem zentralen Ansatz einfach und elegant.

Zustandsänderungen bei asynchronen Programmabläufen lassen sich wesentlich einfacher verwalten, und die Applikation skaliert bei zunehmender Codebasis besser. Die Erfahrung zeigt allerdings auch, dass relativ viel Boilerplate-Code geschrieben werden muss, um die Redux-Architektur zu implementieren.

Ich bin der Meinung, dass es sich lohnt, diesen Aufwand zu betreiben. Die Struktur hat einen wesentlichen Einfluss darauf, wie gut die Webapplikation skaliert und wie gut sie gewartet werden kann. Außerdem gibt es inzwischen Möglichkeiten, diesen Boilerplate-Code mithilfe der Angular-CLI zu generieren: ng g store State --root --module app.module.ts

Wie lief das State-Handling in deinem letzten Projekt?

Welche Redux-Integration findest du besser?

Schreib mir deine Erfahrung als Kommentar!

Kommentare