Agenda:
- Was ist NGRX?
- Vorteile der Verwendung von NGRX
- Nachteile der Verwendung von NGRX
- Wann man NGRX verwendet...
- Actions, Reducers, Selectors, Store und Effects + Beispiel-Implementierung
Was ist NGRX?
NGRX ist eine Gruppe von Bibliotheken, die vom Redux-Muster "inspiriert" sind. Einfach ausgedrückt bedeutet dies, dass NGRX die angular/rxjs-Version des Redux-Templates ist. Der „rxjs“-Teil besteht darin, dass die NGRX-Implementierung den rxjs-Thread umgeht. Dies bedeutet, dass es mit Observables und verschiedenen Observable-Operatoren arbeitet, die von rxjs bereitgestellt werden. Über rxjs siehe https://angular.io/guide/rx-library.
Der Hauptzweck dieses Schemas besteht darin, einen vorhersagbaren Applikationszustand basierend auf drei Hauptprinzipien bereitzustellen.
Werfen wir dafür einen Blick auf die drei Prinzipien des Redux-Modells und zeigen die wichtigsten Vorteile auf, die sie bieten.
Die einzige Quelle der Wahrheit (Single Source of Truth)
Im Falle der redux/ngrx-Architektur bedeutet dies, dass der Status einer gesamten Anwendung in einem Objektbaum innerhalb eines Repositorys gespeichert wird.
Die Vorteile einer Single Source of Truth sind zahlreich, aber für mich ist der interessanteste (weil dies etwas ist, das sich auf jede Angular-Anwendung auswirkt) der folgende:
- Wenn man eine Angular-App erstellst, teilt man normalerweise den Zustand und verwaltet mehrere Dienste. Wenn eine Anwendung wächst, wird es immer schwieriger, den Überblick über Änderungen an einem Status zu behalten und wird schließlich chaotisch, schwierig zu debuggen und zu warten. Eine Single Source of Truth löst dieses Problem, da der States nur in einem Objekt und an einem Ort behandelt wird, sodass das Debuggen oder Hinzufügen von Änderungen viel einfacher wird.
Unveränderlichkeit
Die Änderungen sind nicht mehr direkt am bisherigen Objekt möglich. Wir sind gezwungen, das Objekt auszutauschen. Hierfür erzeugen wir bei jeder Änderung eine Kopie des vorherigen Objekts, mit einer Ausnahme: dem zu ändernden Wert. Eine Änderung festzustellen, ist nun sehr einfach: Wir müssen lediglich Referenzen vergleichen. Man wird den Status niemals direkt ändern, sondern Aktionen ausführen (das können Dinge sein wie das Abrufen, Hinzufügen, Löschen oder Aktualisieren des Status).
Deterministische Zustandsänderungen (pure functions)
Die durch das Versenden einer Aktion ausgelöste Operation ist eine einfache Funktion (pure function) namens Reducer innerhalb der Redux-Architektur.
Diese Reducer (es handelt sich um einfache Funktionen) erhalten eine Aktion und einen Status. Abhängig von der gesendeten Aktion führen sie die Operation aus und geben ein neues Statusobjekt zurück. Der Status in einer Redux-App ist unveränderlich! Wenn also der Reducer etwas am Zustand ändert, gibt er ein neues Zustandsobjekt zurück. Die Vorteile der Verwendung reiner Funktionen sind bekannt (bspw. die Tatsache, dass sie sofort getestet werden können, wenn man dieselben Argumente übergibt, die man als Ergebnis erhält.
Dieser Ansatz ermöglicht es uns auch, mit den Redux/ngrx-Entwicklungstools zwischen verschiedenen Instanzen unseres Status zu navigieren und unter anderem zu sehen, was sich zwischen den Instanzen geändert hat und wer es geändert hat. Die Verwendung reiner Funktionen und die Rückgabe neuer Zustandsinstanzen hat also auch einen großen Einfluss auf das Debugging. Aber der Hauptvorteil besteht darin, dass wir durch die Bindung aller unserer Komponenteneingaben an Zustandseigenschaften die Änderungserkennungsstrategie ändern können, um sie zu pushen. Das verbessert die Leistung der Anwendung.
Vorteile der Verwendung von NGRX
Die wichtigsten Vorteile der Verwendung von Redux-Vorlagen in einer Anwendung:
- Da wir über eine „Single Source of Truth“ verfügen und den Status nicht direkt ändern können, funktionieren Anwendungen konsistenter.
- Die Verwendung der Redux-Vorlage bietet viele nützliche Funktionen, um das Debuggen zu vereinfachen.
- Das Testen von Anwendungen wird einfacher, weil wir einfache Funktionen zum Umgang mit Zustandsänderungen einführen und sowohl NGRX als auch rxjs viele großartige Testfunktionen haben.
- Sobald man sich mit NGRX vertraut gemacht hat, wird das Verständnis des Datenflusses in den Anwendungen unglaublich einfach und vorhersehbar.
Nachteile der Verwendung von NGRX
- NGRX hat sicherlich eine Lernkurve. Ncht zu groß, aber auch nicht zu klein, erfordert es etwas Erfahrung bzw. ein tiefes Verständnis einiger Programmiermuster. Für einen Junior-Entwickler kann das anfangs etwas verwirrend sein.
- Jedes Mal, wenn man dem Zustand eine Eigenschaft hinzufügt, muss man Aktionen, Dispatcher hinzufügen, muss man möglicherweise Selektoren, Effekte, falls vorhanden, aktualisieren oder hinzufügen, den Speicher aktualisieren. Außerdem führt man überall Verkettung von rxjs-Operatoren und Observablen durch.
- NGRX ist nicht Teil der Angular Core Libraries und wird von Google nicht unterstützt, zumindest nicht direkt, da es ngrx-Mitwirkende im Angular-Team gibt. Es muss nur etwas nachgedacht werden, bevor man eine Bibliothek hinzufügt, die eine große Abhängigkeit für die Anwendung darstellt.
Wann man NGRX verwenden sollte
Der allgemeine Konsens besteht also darin, dass NGRX in mittleren/großen Projekten verwendet werden sollte, wenn es schwierig wird, die Zustandsverwaltung zu warten. Manche Leute, die fanatischer sind, sagen Dinge wie „Wenn du ein Vermögen hast, dann hast du NGRX“.
Ich stimme zu, dass es in mittleren bis großen Projekten verwendet werden sollte, in denen man einen signifikanten Zustand hat und viele Komponenten diesen Zustand verwenden. Man sollte aber bedenken, dass Angular selbst viele Zustandsverwaltungslösungen bietet und wenn man ein starkes Angular-Entwicklungsteam hast, dann braucht man sich vielleicht keine Sorgen um NGRX zu machen. Abgesehen davon glaube ich, dass sich ein starkes Angular-Entwicklungsteam auch dafür entscheiden könnte, NGRX in die Lösung aufzunehmen, weil sie die Leistungsfähigkeit des Redux-Templates sowie die von rxjs-Operatoren hinzugefügte Leistung kennen und sich wohlfühlen, mit beiden zu arbeiten …
Wenn man eine einfache Antwort erwartet, um zu entscheiden, wann man NGRX verwenden sollte, wird man keine bekommen, und man wird niemandem außerhalb der eigenen Organisation oder Gruppe vertrauen, der einem diese Antwort gibt. Für eine Entscheidung muss man die Vor- und Nachteile abwägen, das eigene Team verstehen und seine Meinung berücksichtigen.
NGRX-Actions, -Reducers, -Selectors, -Store, -Effects mit Beispiel-Implementierung
Ich habe das Beispiel aus diesem Kapitel auf GitHub zur Verfügung gestellt, sodass man den Code vollständig nachvollziehen kann: https://github.com/ArkadiRosenberg/NgrxDemo
Der Datenfluss in der Redux-Architektur ist in folgender Abbildung dargestellt:
Hier ist gut erkennbar, dass die Daten stets in eine Richtung fließen und dass Lesen und Schreiben klar voneinander getrennt sind.
Unser Beispiel enthält eine Liste von Benutzern, die Benutzerdetailseite und einige anfängliche Konfigurationsinformationen, die man beim Start der App abrufen muss. Wir werden in der Lage sein, einige wichtige NGRX-Flows zu implementieren.
Dies sind die Dinge, die wir tun werden:
- Installation der Bibliothek
- Erstellen einer Ordnerstruktur für die Speicherung
- Erstellen von Speicher- und Anfangswerten
- Actions erstellen
- Reducer erstellen
- Effects erstellen
- Selectors erstellen
- Endgültige Einstellung
- Verwenden von Speicher in Komponenten
Also los...
Projekt erstellen:
ng new ngrx-demo --style=scss
Füge die NGRX-Bibliotheken hinzu, die wir verwenden werden:
npm install @ngrx/store @ngrx/effects @ngrx/store-devtools @ngrx/router-store --save
Struktur des States definieren
Beginnen wir mit der Erörterung der Speicherdateistruktur. Diese Dateistruktur und alle Speicherkonfigurationen müssen im Hauptmodul der Anwendung vorhanden sein.
Die Ordnerstruktur ist eine Darstellung der tatsächlichen Zustands der Anwendung. Man wird einen Hauptordner namens State und fünf Dateien haben, die jeden der Hauptakteure des Stores darstellen: Actions, Effekts, Reducer und Selectors.
Erstellen von State und Initialwerten
Wie bereits erwähnt, werden wir zwei Hauptabschnitte über unsere Anwendung Benutzer und Konfiguration haben. Für beide müssen wir den Status und den Anfangsstatus erstellen, und wir müssen dasselbe auch für den App-Status tun.
Hier ist ein Beispiel für User-Status:
exportinterfaceUserState {
users: User[];
selectedUser: User | null;
isLoading: boolean;
}
exportconstinitialUserState: UserState = {
users: [],
selectedUser:null,
isLoading:false,
};
Wir haben zwei solche Schnittstellen erstellt, um Benutzer und Konfiguration zu definieren:
exportinterfaceUserState {
users: User[];
selectedUser: User | null;
isLoading: boolean;
}
exportinterfaceConfigState {
config: Config | null;
isLoading: boolean;
}
Actions: Kommunikation mit dem Store
Alle relevanten Ereignisse in der Anwendung werden durch Actions repräsentiert, die in den Store gesendet werden. Das umfasst Aktionen, die direkt vom Nutzer ausgeführt werden, und auch technische Ereignisse wie Antworten von der http-Schnittstelle.
Actions bilden die Grundlagen für die Kommunikation mit dem Store und können Änderungen am Anwendungszustand auslösen.
Die Beispiel Actions aus der user-actions.ts Datei sehen wie folgt aus:
exportconstloadUsers = createAction('[User] Load Users');
exportconstloadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
exportconstloadUsersFailure = createAction(
'[User] Load Users Failure',
props<{ error: any }>()
);
Dispatch: Actions in den Store senden
Um mit dem Store zu kommunizieren und Zustandsänderungen anzustoßen, müssen die Actions von den Komponenten über Facade in den Store gesendet werden:
publicgetUsers() {
this.userStore.dispatch(UserActions.loadUsers());
}
Facade Pattern
Facade ist ein Muster, das eine einfache öffentliche Schnittstelle bereitstellt, um eine komplexere Verwendung zu kapseln.
Da wir NGRX mehr und mehr in unserer Anwendung verwenden, fügen wir mehr Actions und mehr Selectors hinzu, die unsere Komponenten verwenden und verfolgen müssen. Dies erhöht die Kopplung zwischen unserer Komponente und den Aktionen und Selektoren selbst.
Das Facade-Muster möchte diesen Ansatz vereinfachen, indem es die NGRX-Interaktionen an einer Stelle zusammenfasst, sodass die Komponente immer nur mit dem Facade interagieren kann. Dies bedeutet, dass Du die NGRX-Artefakte frei umgestalten kannst, ohne sich Gedanken über die Beschädigung Ihrer Komponenten machen zu müssen.
Reducer: den State aktualisieren
Nachdem wir die erste Action in den Store dispatcht haben, ist es nun an der Zeit, einen Reducer zu entwickeln, um den State zu verändern. Ein Reducer im Kontext von Redux ist eine Funktion mit Zwei Eingabewerten: der aktuelle Zustand und die neue eintreffende Action. Die Aufgabe des Reducers ist es, anhand der Action und des Zustands einen neuen Zustand zu berechnen und zurückzugeben.
exportconstuserReducer = createReducer(
initialUserState,
on(
UserActions.loadUsers,
(state): UserState=> ({
...state,
isLoading:true,
})
),
on(
UserActions.loadUsersSuccess,
(state, action): UserState=> ({
...state,
users:action.users,
isLoading:false,
})
),
on(
UserActions.loadUsersFailure,
(state): UserState=> ({
...state,
isLoading:false,
})
),
on(
UserActions.selectUser,
(state, action): UserState=> ({
...state,
selectedUser:action.user,
})
)
);
Selektoren: Daten aus dem State lesen
Der NGRX Store bietet uns die Funktion Selectors, um Teile unseres Stores zu erhalten. Aber was ist, wenn wir etwas Logik auf dieses Slice anwenden müssen, bevor wir die Daten in den Komponenten verwenden?
Dort werden die Selektoren tätig. Sie ermöglichen es uns, jede State-Slice-Datentransformation von den Komponenten zu entkoppeln. Die store select-Funktion akzeptiert als Argument eine reine Funktion, diese reine Funktion ist unser Selektor:
this.users$ = this.userStore.select(selectUserList);
Hier ist ein Beispiel von einem Selector:
import { createFeatureSelector, createSelector } from'@ngrx/store';
import { userFeatureKey, UserState } from'./user-reducer';
exportconstselectUserState = createFeatureSelector<UserState>(userFeatureKey);
exportconstselectUserList = createSelector(
selectUserState,
(state: UserState) =>state.users
);
exportconstselectSelectedUser = createSelector(
selectUserState,
(state: UserState) =>state.selectedUser
);
In einem solchen Selektor versteckt sich außerdem ein wichtiges Konzept, das sich Memoization nennt. Damit aufwendige Projektionen nicht bei jeder State-Änderung neu ausgeführt werden, speichert jeder Selektor seine zuletzt verarbeiteten Eingabewerte.
Effects: Seiteneffekte ausführen
Effects im Ökosystem der ngrx-Bibliotheken ermöglichen es uns, mit Nebenwirkungen umzugehen, die durch das Senden einer Aktion außerhalb von Angular-Komponenten oder des NGRX-Store verursacht werden.
Sobald der Effekt aktiv ist, wird unsere Anwendung die Benutzer über HTTP laden und die Liste mittels loadUsersSuccess an den Reducer übergeben:
@Injectable()
exportclassUserEffects {
constructor(
privateactions$: Actions,
privateuserService: UserService,
privatestore: Store<AppState>
) {}
getUsers$ = createEffect(() => {
returnthis.actions$.pipe(
ofType(UserActions.loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
map((data) =>
UserActions.loadUsersSuccess({
users:data.users,
})
),
catchError((err) =>of(UserActions.loadUsersFailure({ error:err })))
)
)
);
});
}
Dieser wird einen neuen Zustand mit den geladenen Benutzern berechnen. Abschließend wird mithilfe eines Selektors die Oberfläche aktualisiert, und die neue Benutzerliste wird angezeigt:
Fazit
Mithilfe von Redux und NGRX kann man die Zustandsverwaltung in der Anwendung zentralisieren. Redux setzt auf ein unveränderliches Zustandsobjekt, das mithilfe von Pure Functions im Store verwaltet wird. Alle Ereignisse der Anwendung werden durch Actions signalisiert, die Änderungen am Zustand auslösen können. Dabei erzeugen die Reducer einen neuen Zustand, der über ein Observable an alle Abonnenten ausgegeben wird. Die Teile der Architektur sind stark entkoppelt, sodass sie unabhängig voneinander entwickelt und gewartet werden können.
Hoffentlich gibt dies eine Einführung in NGRX-Store und die Vor- und Nachteile ihrer Verwendung. Dies sollte helfen zu beurteilen, ob man sie verwendet oder nicht.
Weiterführende Internetseiten und Literatur:
- https://levelup.gitconnected.com/angular-ngrx-a-clean-and-clear-introduction-4ed61c89c1fc/
- Angular: Das große Praxisbuch – Grundlagen, fortgeschrittene Themen und Best Practices. Inkl. RxJS, NgRx und a11y (iX Edition). Von Ferdinand Malcher , Danny Koppenhagen , Johannes Hoppe
- Angular: Das große Handbuch zum JavaScript-Framework. Einführung und fortgeschrittene TypeScript-Techniken. Inkl. Angular Material. Von Christoph Höller.
Das Quellcode:
Dies ist ein Beitrag von Arkadi Rosenberg, QUIBIQ Stuttgart.