12. Flux

Dieser Abschnitt behandelt die Grundidee hinter Flux. Betrachtet wird die Implementierung Redux für React und @ngrx für Angular.

12.1 Einführung

Redux ist die bekannteste Umsetzung von Flux und der ursprünglichen, ersten Flux-Implementierung – die auch den Namen Flux etablierte – in vielerlei Hinsicht überlegen.

12.1.1 Ein Zustandsbaum

Es ist fundamental für Redux, dass alle Zustände einer Applikation ein einem zentralen JavaScript-Objekt gehalten werden. Üblicherweise ist dies eine Objektstruktur – der Zustandsbaum. Dieses Objekt wird auch als Store oder Application-Store bezeichnet. Die Zustände in diesem Objekt sind grundsätzlich unveränderlich (immutable). Um eine Zustandsänderung durchzuführen, werden sogenannte Reducer benutzt. Das sind Funktionen zum Verändern der Zustände.

12.1.2 Der Ereignisfluss

In Redux werden Benutzeraktionen gefangen und an Reducer zur Verarbeitung übertragen. Die Last der Eingabeverarbeitung liegt damit nicht mehr beim Controller oder der Komponente. Viele Benutzeraktionen führen lediglich zu Zustandsänderungen. Diese Vorgänge sind nicht unbedingt als Teil der Geschäftslogik zu betrachten. Die Trennung vereinfacht die Controller und damit die Komponenten der Applikation.

Ereignisse fließen in der Applikation nach oben, vom DOM zur Logik. Werden Komponenten verschachtelt, fließen Ereignisse von der Kindkomponente zur Elternkomponente. Werden Dienste benutzt, beispielsweise zum asynchronen Abruf von Daten von einem Server, werden die Ergebisse als Ereignis ausgesendet und an den Reducer geliefert.

12.1.3 Der Zustandsfluss

Zustände fließen nach unten, also entgegen den Ereignissen. Die Zustände in einer Elternkomponente wirken also auf die Kindkomponenten, wenn dies erforderlich ist, jedoch niemals umgekehrt.

Der Status der Applikation ist Schlüssel der Stabilität. Die Überlegung, die dahinter steckt, lässt sich in einer Aussage zusammenfassen:

The Single Source of Truth

Damit ist gemeint, dass der Zustand der Applikation – bestimmt durch den Zustand vieler Komponenten – nicht über die gesamte Applikation verteilt ist, sondern es einen zentralen Puntk gibt, an dem alles zusammenfließt. Und nicht nur das. Der Status ist “readonly” – also aus Objektsicht unveränderlich. Änderungen erfordern zwingend Funktionen, das Objekt selbst ist tabu.

Diese Funktionen werden “reducer” genannt. Damit ist der Datenfluss klar geregelt und es werden Seiteneffekte vermieden. Seiteneffekte sind das, was in Applikation schnell als unklares Verhalten wahrgenommen wird.

Abbildung: Das Prinzip des unidirektionalen Datenflusses
Abbildung: Das Prinzip des unidirektionalen Datenflusses

Als Bibliothek zum Abbilden des Status wird meist Redux oder @ngrx benutzt. Redux meist mit React, @ngrx dagegen mit Angular. Es ist unabhängig, wird aber oft zusammen benutzt.

12.2 Redux mit React

Redux ist eine Bibliothek zur Verwaltung von Zuständen. Sie ist unabhängig von Angular, React und von jedem anderen Framework. Das Zusammenspiel mit Angular ist jedoch besonders spannend und hier behandelt. Zuerst werden die Grundlagen isoliert betrachtet, um den Einstieg so einfach wie möglich zu halten.

12.2.1 Actions

Grundlage für Redux ist ein fundamentales Anweisungsobjekt:

1 const ADD_TODO = 'ADD_TODO';

Daraus wird nun die Action:

1 { 
2   type: ADD_TODO, 
3   text: 'Build my first Redux app' 
4 } 

Dies propagiert zwei Angaben:

12.2.2 Reducer

Reducer, die dies verarbeiten, sind reine Funktionen. Du hast hier wieder alle Freiheitsgrade. Der Trick besteht in der Kontrolle des Datenflusses, nicht in der Kontrolle der Daten selbst. Hier ein Beispiel für Reducer, Exportfunktionen und Bereitstellung:

 1 import {
 2   ADD_TO_CART,
 3   CHECKOUT_REQUEST,
 4   CHECKOUT_FAILURE
 5 } from '../constants/ActionTypes'
 6 
 7 const initialState = {
 8   addedIds: [],
 9   quantityById: {}
10 }
11 
12 const addedIds = (state = initialState.addedIds, action) => {
13   switch (action.type) {
14     case ADD_TO_CART:
15       if (state.indexOf(action.productId) !== -1) {
16         return state
17       }
18       return [ ...state, action.productId ]
19     default:
20       return state
21   }
22 }
23 
24 const quantityById = (state = initialState.quantityById, action) => {
25   switch (action.type) {
26     case ADD_TO_CART:
27       const { productId } = action
28       return { ...state,
29         [productId]: (state[productId] || 0) + 1
30       }
31     default:
32       return state
33   }
34 }
35 
36 export const getQuantity = (state, productId) =>
37   state.quantityById[productId] || 0
38 
39 export const getAddedIds = state => state.addedIds
40 
41 const cart = (state = initialState, action) => {
42   switch (action.type) {
43     case CHECKOUT_REQUEST:
44       return initialState
45     case CHECKOUT_FAILURE:
46       return action.cart
47     default:
48       return {
49         addedIds: addedIds(state.addedIds, action),
50         quantityById: quantityById(state.quantityById, action)
51       }
52   }
53 }
54 
55 export default cart

addedIds und quantityById sind die Reducer-Funktionen. cart ist die Funktion, die von außen aufgerufen wird, um die Reducer auszulösen. Der gesamte Code ist zu viel, um hier abgedruckt zu werden. Es sollte lediglich ein Eindruck erweckt werden, wie das praktisch aussieht.

12.3 @ngrx/store mit Angular

In diesem Abschnitt wird NGRX vorgestellt. Es ist empfehlenswert, die Grundlagen von Angular in den vorherigen Kapiteln zu lesen – dies wird hier vorausgesetzt.

12.3.1 Einführung

Im Vergleich mit der Kombination React/Redux erscheint NGRX klarer und strukturierter. Es gibt weniger Freiheiten und mehr Vorgaben. Die Benutzung der TypeScript-Dekoratoren erhöht deutlich die Lesbarkeit des Codes. Es lohnt sich also, diese Version zu benutzen und nicht direkt mit Redux zu arbeiten. NGRX nutzt RXJS, sodass alle Aufrufe asynchron und mittels Observables ausgeführt werden.

12.3.1.1 Installation

Wie üblich ist die Bibliothek via npm verfügbar.

npm i @ngrx/store

Nach der Bereitstellung müssen die Packer oder Loader wie WebPack oder SystemJS angepasst werden.

12.3.2 Einsatz

Kern der Nutzung ist die Action. Actions werden an den Store dispatched (gesendet). Der Store wird durch diese Aktion aktualisiert. Ein direkter Zugriff auf den Store, der den Zustand der gesamten Applikation enthält, ist nach dem Flux-Pattern nicht erlaubt. Ein einfaches Beispiel uzeigt, wie dies aussieht:

 1 import {Injectable} from '@angular/core';
 2 import {Action} from '@ngrx/store';
 3 
 4 import {Hero} from '../models';
 5 
 6 @Injectable()
 7 export class HeroActions {
 8     static LOAD_HEROES = '[Hero] Load Heroes';
 9     loadHeroes(): Action {
10         return {
11             type: HeroActions.LOAD_HEROES
12         };
13     }
14 
15     static LOAD_HEROES_SUCCESS = '[Hero] Load Heroes Success';
16     loadHeroesSuccess(heroes): Action {
17         return {
18             type: HeroActions.LOAD_HEROES_SUCCESS,
19             payload: heroes
20         };
21     }
22 
23     static GET_HERO = '[Hero] Get Hero';
24     getHero(id): Action {
25         return {
26             type: HeroActions.GET_HERO,
27             payload: id
28         };
29     }
30 
31     static GET_HERO_SUCCESS = '[Hero] Get Hero Success';
32     getHeroSuccess(hero): Action {
33         return {
34             type: HeroActions.GET_HERO_SUCCESS,
35             payload: hero
36         };
37     }
38 
39     static RESET_BLANK_HERO = '[Hero] Reset Blank Hero';
40     resetBlankHero(): Action {
41         return {
42             type: HeroActions.RESET_BLANK_HERO
43         };
44     }
45 
46     static SAVE_HERO = '[Hero] Save Hero';
47     saveHero(hero): Action {
48         return {
49             type: HeroActions.SAVE_HERO,
50             payload: hero
51         };
52     }
53 
54     static SAVE_HERO_SUCCESS = '[Hero] Save Hero Success';
55     saveHeroSuccess(hero): Action {
56         return {
57             type: HeroActions.SAVE_HERO_SUCCESS,
58             payload: hero
59         };
60     }
61 
62     static ADD_HERO = '[Hero] Add Hero';
63     addHero(hero): Action {
64         return {
65             type: HeroActions.ADD_HERO,
66             payload: hero
67         };
68     }
69 
70     static ADD_HERO_SUCCESS = '[Hero] Add Hero Success';
71     addHeroSuccess(hero): Action {
72         return {
73             type: HeroActions.ADD_HERO_SUCCESS,
74             payload: hero
75         };
76     }
77 
78     static DELETE_HERO = '[Hero] Delete Hero';
79     deleteHero(hero): Action {
80         return {
81             type: HeroActions.DELETE_HERO,
82             payload: hero
83         };
84     }
85 
86     static DELETE_HERO_SUCCESS = '[Hero] Delete Hero Success';
87     deleteHeroSuccess(hero): Action {
88         return {
89             type: HeroActions.DELETE_HERO_SUCCESS,
90             payload: hero
91         };
92     }
93 }

Actions leiten also von einer Basisklasse Action ab. Die Konstanten (static) verringern die Fehlerquote beim Tippen, sind also nicht primär funktional. Der Anwendung werden die Actions dann als Dienst mittels @Injectable() bereitgestellt (vergiß nicht die Registrierung im Modul).

Actions haben eine sogenannte Last (Payload). Diese kann leer sein, wenn die Action selbst keine Daten bereitstellt, sondern nur etwas auslöst. Im Beispiel haben die Rückgabeobjekte der Actions deshalb die Eigenschaften type (Was ist zu tun?) und optional payload für die Daten. Die Daten werden beim Aufruf der Action übergeben, sie sind Parameter.

Der Grund für diese Art der Bereitstellung ist Bequemlichkeit. Bei der Benutzung erweist sich das Pattern als außerordentlich praktisch. In der Komponente kommt die Action nun zur Anwendung.

 1 @Component({
 2     ...
 3 })
 4 export class SomeComponent implments OnInit {
 5     constructor(
 6         private heroActions: HeroActions,
 7         private store: Store<AppState>
 8     ) {}
 9 
10     ngOnInit() {
11         this.store.dispatch(this.heroActions.loadHeroes());
12     }
13 }

Das verbindende Element ist hier die Klasse Store. An den Store erfolgt das Senden (dispatch) der Action. So wird der Store über einen Änderungswunsch informiert. Es ist Sache der Reducer, dann für die adäquate Reaktion zu sorgen.

12.3.3 Reducer

Nur der Reducer darf den Store ändern. Komponenten, Direktiven, Dienste oder Actions dürfen dies nicht. So kommt Ordnung und Struktur in die Applikation. Fehlfunktionen musst du dann nicht mehr irgendwo suchen – es reicht, sich die Reducer anzuschauen. Reducer sind reine Funktionsaufrufe, sogenannte “pure function calls”. Es ist die Natur derartiger Funktionen, keine Seiteneffekte zu haben. Die Funktion gibt den neuen Status als neues Objekt zurück, vollkommen vom vorherigen entkoppelt. Zwischen Komponente und Store pendeln also unabhängige Objekte, an denen versehentliche Änderungen wirkungslos bleiben.

Nun ein Blick auf einen typischen Reducer. Zuerst werden die Reducer der Anwendung zentral registriert. Der Vorgang wird hier als compose bezeichnet:

Listing: reducers/index.ts
 1 //importe entfernt
 2 
 3 import heroListReducer, * as fromHeroList from './hero-list';
 4 import heroReducer, * as fromHero from './hero';
 5 
 6 export interface AppState {
 7     heroes: fromHeroList.HeroListState;
 8     hero: fromHero.HeroState;
 9 };
10 
11 export default compose(combineReducers)({
12     heroes: heroListReducer,
13     hero: heroReducer
14 });

Es ist sinnvoll, die Reducer einzeln zu definieren und dann zusammenzufassen. Jeder Reducer hat genau eine Aufgabe und es ist sehr schlechter Stil, mittels des Payloads hier weitere Effekte auszulösen, nur weil man zu faul ist, Reducer aufzuteilen.

Im Code ist noch ein weiterer Typ definiert, AppState. Dies ist die Definition der Struktur der Status, der im Store gehalten wird. Im Beispiel ist dies eine Liste von Objekten und ein weiteres, dass gerade ausgewählt oder aktiv ist.

Der Reducer selbst ist der Teil, wo mit echten Daten was passiert. Sinnvoll sind deshalb passende Typen:

Listing: reducers/hero-list.ts
1 //importe entfernt
2 
3 export type HeroListState = Hero[];
4 
5 const initialState: HeroListState = [];

HeroListState ist hier die Form des Speicherobjekts des Status. Der Reducer wird dies später erzeugen und der Store legt es dann sicher ab. Am Anfang ist das Objekt leer. Der Anfangszustand ist allerdings beliebig. Es ist nicht unüblich, den Daten noch Metadaten mitzugeben, um dynamische Prozesse abzubilden. Eigenschaften wie loading oder deleting könnten durch Actions temporär gesetzt werden, um die Zustände während des Ladens oder einer anderen API-Aktion anzuzeigen. Eine Fortschrittsbalken-Komponente könnte dann den Zustand des Store abfragen, und bei Auftreten des Zustands loading einen wandernden Balken anzeigen. Mit dem eigentlichen Ladevorgang hat diese Komponente nichts zu tun und es besteht eine vollständige Entkopplung. Entscheidet der Produktmanager später, dass diese Visualisierung nicht mehr benötigt wird, wird die Fortschrittsbalken-Komponente entfernt und dies hat keinerlei Einfluss auf andere Teile der Applikation. Die Gefahr von Fehlern ist signifikant reduziert.

Der eigentliche Reducer ist, wie bereits erwähnt, eine einfache Funktion:

1 export default function (state = initialState, action: Action): Hero\
2 ListState {
3     switch (action.type) {
4         ...
5     }
6 }

Der Funktion wird die Action übergeben, damit klar ist, was mit welchen Daten geschehen soll. Der Reducer tut etwas und gibt einen neuen Status zurück. Der erste Parameter ist der Anfangsstatus. Nun zu den Aufgaben des Reducers.

1 case HeroActions.LOAD_HEROES_SUCCESS: {
2     return action.payload;
3 }
4 case HeroActions.ADD_HERO_SUCCESS: {
5     return [...state, action.payload];
6 }

Der erste Aufruf bestätigt lediglich die Ausführung. Die zweite hängt das neue Element an die bestehende Liste an (Array-Kombination mit Spread-Operator).

Eine Speicherfunktion könnte folgendermaßen aussehen (_ ist die LoDash Bibliothek, die Funktionen zum Arbeiten mit Daten bereitstellt):

 1 case HeroActions.SAVE_HERO_SUCCESS: {
 2     let index = _.findIndex(state, {id: action.payload.id});
 3     if (index >= 0) {
 4         return [
 5             ...state.slice(0, index),
 6             action.payload,
 7             ...state.slice(index + 1)
 8         ];
 9     }
10     return state;
11 }

Der etwas eigenwillig anmutende Rückgabewert trägt der Tatsache Rechnung, das der Status absolut unveränderlich ist und der neue Status komplett neu zusammengebaut werden muss. Der Zustand kann neben solch komplexen Arrays aber auch schlicht ein Boolescher Wert sein. Meist wird jedoch eine Liste von Daten manipuliert.

1 case HeroActions.DELETE_HERO_SUCCESS: {
2     return state.filter(hero => {
3         return hero.id !== action.payload.id;
4     });
5 }

Für unbekannte Actions wird noch eine Rückfallebene eingebaut, die den Status unverändert lässt.

1 default: {
2     return state;
3 }

Das bisherige Beispiel behandelte die gesamte Liste. Änderungen an einzelnen Objekten funktionieren vergleichbar.

1 // /app/reducers/hero.ts
2 ...
3 export type HeroState = Hero;
4 
5 const initialState: HeroState = {
6     id: 0,
7     name: ''
8 };

Hier der Reducer für das Zurücksetzen und Laden:

 1 export default function (state = initialState, action: Action): Hero\
 2 State {
 3     switch (action.type) {
 4         case HeroActions.RESET_BLANK_HERO: {
 5             return initialState;
 6         }
 7         case HeroActions.GET_HERO_SUCCESS: {
 8             return action.payload;
 9         }
10         default: {
11             return state;
12         }
13     }
14 }

Nun nützt das alles meist nichts, wenn die Daten nicht zum Server gelangen. Dies wird im nächsten Abschnitt behandelt.

12.3.4 Server-Dienste nutzen

Der Einstiegspunkt ist eine klassische Dienstdefinition in Angular. Benutzt wird das HttpModule und auf der Serverseite eine API (die kannst du wie im Buch beschrieben mit NodeJS und Express bauen) :

Listing: services/hero.ts
 1 // importe entfernt
 2 
 3 @Injectable()
 4 export class HeroService {
 5     constructor(private http: Http) {}
 6 
 7     getHeroes(): Observable<Hero[]> {
 8         return this.http.get('/api/heroes')
 9         .map(res => res.json());
10     }
11 
12     getHero(id): Observable<Hero> {
13         return this.http.get('/api/heroes/' + id)
14         .map(res => res.json());
15     }
16 
17     saveHero(hero) {
18         if (hero.id === 0) {
19             return this.http.post('/api/heroes', hero)
20             .map(res => res.json());
21         } else {
22             return this.http.put('/api/heroes/' + hero.id, hero)
23             .map(res => res.json());
24         }
25     }
26 
27     deleteHero(hero) {
28         return this.http.delete('/api/heroes/' + hero.id)
29         map(res => hero);
30     }
31 }

So einfach geht es aber nicht, denn diese Funktionen haben ein Problem. Sie nehmen Werte an und geben etwas zurück. Es geht rein und raus. Flux verlangt aber eine Einbahnstraße. Zum Verbinden fehlt also etwas technisches, eine Art Umleitungselement. Dieses Element sind sogenannte Effekte.

12.3.5 Effekte

Technisch besteht der Vorgang des Ladens von Daten aus zwei Aktionen. Zum einen wird der Ladevorgang iniziiert – das ist die LOAD-Action. Dann werden irgendwann vom Server die Daten empfangen. Falls die Liste der Daten nicht genau dem Zustand im Store entspricht, wird der Store aktualisiert. Das Senden oder Empfangen erfolgt immer asynchron. Natürlich könnte man dies zusammenfassen, aber der Auslöser und der Empfänger würden dann Logik in der Komponente erfordern (Laden hat begonnen, Daten treffen ein, Fehler trat auf, Daten sind anders, Daten müssen verteilt werden, …). Genau diese Komplexität in der Komponente soll jedoch unbedingt vermieden werden. Es würde auch das Konzept der totalen Entkopplung kontakarieren.

Effekte stammen aus der Bibliothek @ngrx/effects (kurz: Effects). Es handelt sich hier um ein Verfahren, bei dem absichtlich und bewusst Seiteneffekte (daher der Name) erzeugt werden. Normalerweise versucht man alles, um Seiteneffekte zu vermeiden. Dies macht Software an einigen Stellen jedoch sehr komplex – bis hin zur Unbeherrschbarkeit. Bewusste Seiteneffekte sind die Lösung. Konkret werden zwei Aktionen verbunden. Zum einen der Auslöser für den Ladevorgang mit dem Auslöser der Reaktion auf eintreffende Daten. Effekte sind eine Art Ereignisbehandler. Effekte hören auf das Auftreten von Actions. Tritt Action A auf, beginnt der Effekt auf das Ergebnis einer anderen Action zu warten.

Ein Effekt wird mit Hilfe einer injizierbaren Klasse definiert:

Listing: /app/effects/hero.ts
1 @Injectable()
2 export class HeroEffects {
3     constructor (
4         private update$: StateUpdates<AppState>,
5         private heroActions: HeroActions,
6         private svc: HeroService
7     ) {}
8     ...
9 }

Verbunden wird der Effekt mit dem Dienst, der die Daten per HTTP abruft (HeroService, Zeile 6), den akzeptierten Actions (HeroActions*”, Zeile 5) und dem Reducer, der die Action akzeptiert und ausführt. Der Dekorator @Effect() wird dann benutzt, um diese Kombination zu laden.

1 ...
2 @Effect() loadHeroes$ = this.update$
3     .ofType(HeroActions.LOAD_HEROES)
4     .switchMap(() => this.svc.getHeroes())
5     .map(heroes => this.heroActions.loadHeroesSuccess(heroes));

An dieser Stelle kommt der Benutzung von RXJS eine besondere Bedeutung zu. Die Operatoren, die hier häufig benutzt werden, sind:

Das Auflösen des inneren Observables ist hier der Aufruf des Dienstes. Praktisch wartet switchMap also, bis der Dienst zurückkehrt. Von der semantischen Seite her wird der Aufruf synchron – technisch natürlich nicht. Dies spart das Auslösen einer zweiten Action für den Rückruf vom Server. Der Code setzt nach Eintreffen der Daten unmittelbar mit map fort. Dort wird nun mit den frisch empfangenen Daten in der Tat eine weitere Action ausgelöst, denn die Daten müssen nun in den Store und das geht nur über einen weiteren Reducer. Die Action loadHeroesSuccess löst diesen Reducer aus.

Der Operator ofType stammt aus NGRX und kapselt lediglich einen Aufruf des RXJS-Operators filter.

Die anderen Effekte des Beispiels sind sehr ähnlich aufgebaut:

 1 // importe entfernt
 2 
 3 @Effect() getHero$ = this.update$
 4     .ofType(HeroActions.GET_HERO)
 5     .map<string>(toPayload)
 6     .switchMap(id => this.svc.getHero(id))
 7     .map(hero => this.heroActions.getHeroSuccess(hero));
 8 
 9 @Effect() saveHero$ = this.update$
10     .ofType(HeroActions.SAVE_HERO)
11     .map(update => update.action.payload)
12     .switchMap(hero => this.svc.saveHero(hero))
13     .map(hero => this.heroActions.saveHeroSuccess(hero));
14 
15 @Effect() addHero$ = this.update$
16     .ofType(HeroActions.ADD_HERO)
17     .map(update => update.action.payload)
18     .switchMap(hero => this.svc.saveHero(hero))
19     .map(hero => this.heroActions.addHeroSuccess(hero));
20 
21 @Effect() deleteHero$ = this.update$
22     .ofType(HeroActions.DELETE_HERO)
23     .map(update => update.action.payload)
24     .switchMap(hero => this.svc.deleteHero(hero))
25     .map(hero => this.heroActions.deleteHeroSuccess(hero));

Ergänzend zu der Vorgehensweise soll an dieser Stelle nich die Benutzung in den Komponenten gezeigt werden.

12.3.6 Einsatz von NGRX

Die Architektur des Flux-Modells sorgt dafür, dass die Daten zentral gehalten und definiert verwaltet werden. Dadurch werden die Komponenten schlanker und einfacher. Sie enthalten letztlich nur Anzeigelogik und Benutzeroberfläche. Es gibt zwei Arten von Komponenten:

  1. Container
  2. Display

Die Container-Komponenten dienen der Strukturierung und enthalten keine oder wenig Benutzeroberfläche. Die Display-Komponenten enthalten dagegen HTML und CSS und alles, was eine schöne Benutzeroberfläche benötigt. Wenn zusätzliche Logik benötigt wird, sollte diese vorzugsweise in Container-Komponenten platziert werden.

Eine typische Container-Komponente mit rudimentärer Logik sieht folgendermaßen aus:

Listing: /app/components/heroes/heroes.component.ts
 1 @Component({
 2     ...
 3 })
 4 export class Heroes {
 5     heroes: Observable<any>;
 6     addingHero = false;
 7     selectedHero;
 8 
 9     constructor(
10         private store: Store<AppState>,
11         private heroActions: HeroActions,
12         private router: Router
13     ) {
14         this.heroes = store.select('heroes');
15     }
16 
17     addHero() {
18         this.addingHero = true;
19         this.selectedHero = null;
20     }
21 
22     close() {
23         this.addingHero = false;
24     }
25 
26     delete(hero) {
27         this.store.dispatch(this.heroActions.deleteHero(hero));
28     }
29 
30     select(hero) {
31         this.selectedHero = hero;
32         this.addingHero = false;
33     }
34 
35     gotoDetail() {
36         this.router.go('/detail/' + this.selectedHero.id);
37     }
38 }

Im Grunde passiert hier nicht viel. Es werden Daten aus dem Store geholt und bei Bedarf Actions ausgelöst, um Änderungen vorzunehmen. Die Vorlage enthält kaum HTML, denn dies würde dem Character einer Container-Komponente widersprechen. Stattdessen wird eine Display-Komponente benutzt:

1 <h2>My Heroes</h2>
2 <rx-hero-list
3     [heroes]="heroes | async"
4     [selectedHero]="selectedHero"
5     (onSelect)="select($event)"
6     (onDelete)="delete($event)"
7 ></rx-hero-list>
8 
9 ...

Die Display-Komponente zeigt nun eine Liste von Objekten an. Sie kann sich weiterer Komponenten bedienen. Definiert werden zuerst die Übergabeparameter:

 1 import {Component, Input, Output, EventEmitter} from '@angular/core';
 2 
 3 @Component({
 4     ...
 5 })
 6 export class HeroList {
 7     @Input() heroes;
 8     @Input() selectedHero;
 9 
10     @Output() onSelect = new EventEmitter();
11     @Output() onDelete = new EventEmitter();
12 
13     delete($event, hero) {
14         $event.stopPropagation();
15         this.onDelete.emit(hero);
16     }
17 }

Die Vorlage enthält nun den eigentlichen HTML-Code, der die Anzeige beschreibt. Da es sich um eine Liste handelt, wird *ngFor benutzt.

 1 <ul class="heroes">
 2     <li
 3         *ngFor="let hero of heroes"
 4         (click)="onSelect.emit(hero)"
 5         [class.selected]="hero === selectedHero">
 6         <span class="hero-element">
 7             <span class="badge"></span> 
 8         </span>
 9         <button class="delete-button" (click)="delete($event, hero)"\
10 >Delete</button>
11     </li>
12 </ul>

Der Vorteil liegt erneut in der Einfachheit jedes einzelnen Bausteins. Dies macht sich auch besonders bemerkbar, wenn noch ein Formular zum Ändern der Daten hinzukommt.

 1 @Component({
 2     ...
 3 })
 4 export class HeroForm {
 5     _hero;
 6     @Input() set hero(value) {
 7         this._hero = Object.assign({}, value);
 8     }
 9     get hero() {
10         return this._hero;
11     }
12 
13     @Output() back = new EventEmitter();
14     @Output() save = new EventEmitter();
15 }

Die Besonderheit ist hier der Wert, der zum Befüllen des Formulars benutzt wird. Hier wird das komplette Objekt übergeben. Die Zuweisung mit Object.assign erfolgt, weil der Wert beim Abrufen aus dem Store kommt. JavaScript stellt in solchen Fällen eine Objektreferenz bereit. Das entspricht in C++ einem Pointer. Damit wäre es nun theoretisch möglich, den Wert im Store an allen Kontrollinstanzen vorbei zu verändern. Der Store kann dies effektiv nicht überwachen. Dies ist mehr einer Schwäche der Sprache JavaScript anzulasten, als NGRX. Um hier keine ungewollten Seiteneffekte zu erzeugen, wird eine vollständige Kopie des Objekts erzeugt. Ist der Eingabewert leer (undefined), wird ein leeres Objekt erzeugt ({}).

Angular benutzt in Formularen üblicherweise eine bidirektionale Bindung mit [()]-Ausdrücken oder ngModel. Die Objektzuweisung macht daraus ein unidirektionales Modell, wie es die Flux-Architektur verlangt.

12.3.7 Zusammenfassung

Die Flux-Architektur ist die perfekte Lösung für komplexe Applikationen. Ein zentraler Store dient dazu, den Zustand einer Applikation seiteneffektfrei und sicher zu speichern. Komponenten nutzen Actions, um Änderungswünsche bereitzustellen und Reducer, um dieses technisch umzusetzen. Während redux eine gute Implementierung für React ist, profitieren einfachere Umgebung von der reduzierten Variante MobX. Die Unterschiede werden hier nochmal ausführlich betrachtet.

Wer lieber Angular benutzt, sollte unbedingt zu NGRX greifen, einer an Angular angepassten Version von redux.