NgRx

NgRx #

Präliminarien #

Zustände in einer Webapplikation #

  • Server state
  • Persistent state
  • The URL and router state
  • Client state
  • Transient client state
  • Local UI state

@ngrx/store #

Übersicht #

  • Action: Token, mit dem eine Änderung am state initiiert wird
  • ActionReducerMap: Register für einzelne reducers. Wird in *feature reducers *(üblicherweise in index.ts) erweitert und mit Feature-spezifischen reducers ergänzt. In AppModule durch StoreModule konfiguriert.
  • ActionReducer: Wird benötigt, um reducers wie ein Logger zu erstellen. Wird mit MetaReducer konfiguriert.
  • MetaReducer: Array, welches die action reducers enthält. Wird in AppModule definiert und mithilfe von StoreModule konfiguriert.
  • StoreModule: Modul in @ngrx/store zur Konfiguration von reducers in AppModule
  • createFeatureSelector: Erstellt ein Feature-Selektor für einen state.
  • createSelector: Erstellt einen Selektor als Wrapper für einen state.
  • Store: Stellt Store.select() und Store.dispatch() zur Verfügung, um mit reducers arbeiten zu können. Store.select() wählt einen selector aus, während Store.dispatch() eine action zum reducer schickt.

Action #

Actions sind Nachrichten, die zur Änderungen des state verschickt werden. Sie haben einen type und eventuell auch ein payload-Objekt (im constructor mitgegeben): export interface Action { type: string; }

Typischerweise ist eine actions.ts-Datei folgendermassen aufgebaut:

// 1. Typen
export enum BookActionTypes {
  LoadBook = '[Book Exists Guard] Load Book',
  // Weitere Typen
}

// 2. Actions
export class LoadBook implements Action {
  readonly type = BookActionTypes.LoadBook;

  constructor(public payload: Book) {}
}
// Weitere Actions

// 3. Union Type der Actions
export type BookActionsUnion = LoadBook;

Actions können in folgende drei Kategorien aufgeteilt werden:

  • Commands: Vergleichbar mit dem Aufruf einer Methode: Nur ein Handler zur Verarbeitung der Aktion ist vorgesehen und normalerweise wird ein Rückgabewert (Wert oder Fehler) erwartet. Da es sich um eine “Tätigkeit” handelt, beginnt der Name entsprechender Aktionen in der Regel mit einem Verb
  • Documents: Benachrichtigung über ein Update einer Entität. Ein spezifischer Handler ist nicht vorgesehen (es können durchaus auch mehrere sein). Eine Antwort wird ebenfalls nicht erwartet. Der Name von Documents ist in der Regel ein Substantiv oder ein substantiviertes Verb, und der payload enthält möglicherweise eine Dokumenten-ID oder einen gleichwertigen Identifier
  • Events: Benachrichtigung über einen geschehenen Event. Wie bei Documents ist kein spezifischer Handler vorgesehen, und eine Antwort wird nicht erwartet. Der Name ist eine Beschreibung des Events, und der payload enthält möglicherweise einen Zeitstempel

Für eine einzige Änderung am Zustand werden oftmals verschiedene Actiontypen kombiniert.

ActionReducerMap #

Typ der Map, welche die Reducer-Funktionen eines Moduls enthält. Die entsprechende Instanz wird im Modul mit StoreModule.forRoot bzw. StoreModule.forFeature injiziert. Implementation: export type ActionReducerMap<T, V extends Action = Action> = { [p in keyof T]: ActionReducer<T[p], V> };

Beispiel: export const reducers: ActionReducerMap = { layout: fromLayout.reducer, router: fromRouter.routerReducer, };

ActionReducer #

Interface, welche durch die Reducer-Funktionen implementiert wird

Implementation: export interface ActionReducer<T, V extends Action = Action> { (state: T | undefined, action: V): T; }

Beispiel: export function reducer( state: State = initialState, action: LayoutActions.LayoutActionsUnion ): State { switch (action.type) { case LayoutActions.LayoutActionTypes.CloseSidenav: return { showSidenav: false, };

    case LayoutActions.LayoutActionTypes.OpenSidenav:
      return {
        showSidenav: true,
      };

    default:
      return state;
  }
}

MetaReducer #

MetaReducers sind Wrapper um reducers, welche als “hooks” actions vor der Prozessierung durch den inneren reducer abfangen und eigene Prozesse anstossen können.

Implementation: export type MetaReducer<T, V extends Action = Action> = ( reducer: ActionReducer<T, V> ) => ActionReducer<T, V>;

Beispiel: export function debug(reducer: ActionReducer): ActionReducer { return function(state, action) { console.log(‘state’, state); console.log(‘action’, action);

    return reducer(state, action);
  };
}

StoreModule #

Enthält statische Methoden zur Erstellung von root- bzw. feature-Stores.

forRoot nimmt *reducers *in Form einer ActionReducerMap oder einem InjectionToken<ActionReducerMap> sowie ein optionales Konfigurationsobjekt entgegen: export type StoreConfig<T, V extends Action = Action> = { initialState?: InitialState; reducerFactory?: ActionReducerFactory<T, V>; metaReducers?: MetaReducer<T, V>[]; };

forFeature nimmt zusätzlich als erstes Argument einen featureName entgegen.

createFeatureSelector #

Erstellt einen Selektor des root-Elementes des Feature-Stores. Der Name des Feature-Stores ist sein einziges Argument.

createSelector #

Hilfsfunktion für das Erstellen von Selektoren. Sie sind implementiert für einen bis maximal acht Selektoren sowie jeweils einer Projektorfunktion, welche das gewünschte Feld im Store zurückgibt. Rückgabewert der ganzen Funktion ist jeweils ein memoisierter Selektor.

Store #

Enthält Funktionen, um Aktionen auszulösen und den State mittels Selektoren abzufragen

select ist eine überladene Funktion zum Abfragen des States. Mögliche Argumentenreihen:

  • Selektorfunktion mit optionalen Properties
  • Ein Tuple mit Selektorliteralen

Store.dispatch() nimmt eine Instanz einer Action entgegen

@ngrx/effects #

Effects haben drei Aufgaben:

  • Entscheiden, wie actions prozessiert werden sollen (action deciders)
    • Filtering deciders: Filtern actions basierend auf dem Typ der action. In NgRx gibt es dafür einen speziellen Operator: ofType
    • Content-based deciders: Entscheiden auf Grund des Inhalts (payloads), welche weitere action(s) eine action auslösen soll
    • Context-based deciders: Entscheiden auf Grund von Umgebungsinformationen (bspw. aus Environment-Dateien), welche action(s) eine action auslösen soll
    • Splitter: Wandelt eine action in einen Array von actions um. Dies ist nützlich, um eine action besser zu testen oder zu beobachten
    • Aggregator: Aggregiert verschiedene actions zu einer einzigen (das Gegenteil vom splitter)
  • Transformieren actions in andere actions (action transformers)
    • Content enricher: Reichert eine action mit weiteren Informationen in der payload an (bspw. aus dem Backend)
    • Normalizer & canonical actions: Übersetzt verschiedene actions zu einer einzigen kanonischen action
  • Führen Seiteneffekte aus

Effects ändern aber nie den Zustand der Applikation. Dies ist exklusiv Sache von reducers.

Effects-Klassen wird die Actions-Klasse injiziert, einem observable mit allen actions, welche in der Applikation angestossen werden.

@ngrx/schematics #

Action #

Erstellt eine neue Action-Datei, welche ein enum mit Action-Typen, eine Beispiel-Action-Klasse und eine exportierte Type-Union von Action-Klassen enthält

ng g a ActionName [options]

Optionen:

  • --flat: Action-Datei wird nicht in einem nach der Action benannten Verzeichnis gespeichert (true)
  • -g / --group: Speichert Datei innerhalb eines actions-Verzeichnis (false)
  • --spec: Erstellt zusätzliche spec-Datei (false)

Container #

Erstellt eine Komponente mit injiziertem Store

ng g co ComponentName [options]

Optionen:

  • --state: Pfad zur Datei mit dem State-Interface
  • --stateInterface: Name eienes Interface zur Typisierung des verwendeten State
  • … alle Optionen für Komponenten

Effect #

Erstellt ein Effect-Datei

ng g ef EffectName [options]

Optionen:

  • --flat: Effect-Datei wird nicht in einem nach dem Effect benannten Verzeichnis gespeichert (true)
  • -g / --group: Speichert Datei innerhalb eines effects-Verzeichnis (false)
  • -m / --module: Pfad zum Modul, wo der Effect registriert werden soll
  • --root: Zusammen mit dem -m-Flag verwendet, wird ein Effekt im angegeben Modul als EffectsModule.forRoot registriert (false)
  • --spec: Erstellt zusätzliche spec-Datei (true)

Entity #

Erstellt ein Set von Entity-Dateien: Vordefinierte Actions, ein Collection-Model und ein Reducer mit State-Selektoren

ng g en EntityName [options]

Optionen:

  • --flat: Dateien werden nicht in einem nach der Entity benannten Verzeichnis gespeichert (true)
  • -m / --module: Pfad zum Modul, wo der Reducer registriert werden soll
  • -r / --reducers: Pfad zu einer Reducer-Datei, der ein State-Interface sowie Action Reducers enthält. Das Entity-Interface wird dem ersten Interface in der betreffenden Datei hinzugefügt, der Entity-Reducer wird dem ersten Objekt vom Typ ActionReducerMap hinzugefügt
  • --spec: Erstellt zusätzliche spec-Datei (true)

Feature #

Erstellt ein Feature-Set, welches eine Actions-, eine Effects- und eine Reducer-Datei enthält

ng g f FeatureName [options]

Optionen:

  • --flat: Effects-Datei wird nicht in einem nach dem Feature benannten Verzeichnis gespeichert (true)
  • -g / --group: Speichert Dateien innerhalb eigener Verzeichnise (false)
  • -m / --module: Pfad zum Modul, wo der Feature Reducer registriert werden soll
  • -r / --reducers: Pfad zu einer Reducer-Datei, der ein State-Interface sowie Action Reducers enthält. Das Feature-Interface wird dem ersten Interface in der betreffenden Datei hinzugefügt, der Feature-Reducer wird dem ersten Objekt vom Typ ActionReducerMap hinzugefügt
  • --spec: Erstellt zusätzliche spec-Datei (true)

Reducer #

  • --flat: Reducer-Datei wird nicht in einem nach dem Feature benannten Verzeichnis gespeichert (true)
  • -g / --group: Speichert Reducer-Datei innerhalb eines reducers-Verzeichnis (false)
  • -m / --module: Pfad zum Modul, wo der Reducer registriert werden soll
  • -r / --reducers: Pfad zu einer Reducer-Datei, der ein State-Interface sowie Action Reducers enthält. Das Reducer-Interface wird dem ersten Interface in der betreffenden Datei hinzugefügt, der Reducer selbst wird dem ersten Objekt vom Typ ActionReducerMap hinzugefügt
  • -r / --reducers: Pfad zu einer Reducer-Datei, der ein State-Interface sowie Action Reducers enthält. Das Feature-Interface wird dem ersten Interface in der betreffenden Datei hinzugefügt, der Feature-Reducer wird dem ersten Objekt vom Typ ActionReducerMap hinzugefügt
  • --spec: Erstellt zusätzliche spec-Datei (true)

Store #

Erstellt das initiale Setup für die State-Verwaltung und die Registrierung neuer Feature-States. Integriert zudem @ngrx/store-devtools und generiert eine Datei, welche eine State-Interface, eine ActionReducerMap und assoziierte Meta-Reducers enthält.

ng g st State [options]

Options:

  • -m / --module: Pfad zum Modul, wo der Feature State registriert werden soll
  • --root: Zusammen mit dem -m-Flag verwendet, wird der State im angegeben Modul als EffectsModule.forRoot registriert. true nur für globalen Store verwenden! (false)
  • --statePath: Pfad zum Verzeichnis, wo die State-Datei erstellt werden soll (reducers)
  • --stateInterface: Name eines Interface zur Typisierung des verwendeten State. Falls die --root-Option gewählt wird, wird der Name des Stores verwendet.

Beispielapplikation aus dem Platform-Repository #

Struktur eines Feature-Moduls #

  • actions: Actions im Sinne von Redux
  • components: Presentation components, d.h. Komponenten ohne eigene Logik (ausser @Input() bzw @Output() sowie getters zur Verwendung im Template)
  • containers: Container components, d.h. Komponenten mit eigener Logik (bspw. Organisierung und Orchestrierung der Aktivitäten von Kindkomponenten und Services), die potentiell den Zustand der Applikation verändern. Im Rahmen von NgRx sind dies Komponenten, die actions auslösen und auf Zustandsveränderungen reagieren (mittels select). Im Gegenteil zu presentation components werden sie auch getestet.
  • effects: Services mit Seiteneffekten im Sinne von Redux
  • guards: Route guards
  • pipes: Pipes (in der Beispielapplikation nur im globalen shared-Ordner)
  • models: Models (als interfaces) sowie Funktionen zur Generierung von Mock-Datensätzen
  • reducers: Reducers im Sinne von Redux
  • services: Services, die zur Kommunikation mit externen Diensten (im Fall der Beispielapplikation mit Google Books) verwendet werden
  • xyz.module.ts: @NgModule()-dekorierte Klasse

Weitere Anmerkungen zur Struktur #

  • In fast jedem Verzeichnis werden sog. barrels verwendet. Diese dienen dazu, einen gemeinsamen Import von Klassen in einem Verzeichnis zu ermöglichen.
  • Die app.component wird dem core-Modul hinzugefügt und befindet sich auch in dessen Verzeichnis
  • Im globalen shared-Verzeichnis befindet sich neben dem pipes-Verzeichnis eine Datei utils.ts, welche Utility-Klassen und -Interfaces enthält

Ressourcen #