© tomertu/Shutterstock.com
State-Management mit Angular und NgRx

Stein auf Stein


Die Frontend-Plattform Angular bietet uns viele Möglichkeiten, gut strukturierte, wart- und testbare Architekturen zu entwickeln. Die vielen Bausteine einer Applikation machen das Trennen der Verantwortlichkeiten sehr einfach und nachvollziehbar. Daher ist Angular auch für große Businessapplikationen ein passender Kandidat.

Gerade in großen Businessapplikationen ist das Behandeln des Status (State) der Anwendung eine der größten Herausforderungen, der wir uns beim Schreiben der Applikation stellen können. Um den Überblick zu behalten, ist es wichtig, den aktuellen State der Applikation greifbar zu machen, abzubilden und Änderungen daran nachvollziehbar vorzunehmen.

Um diese komplexe Aufgabe zu meistern, haben sich mit der Entwicklung von Angular Projekte entwickelt, die sich dieses Problems annehmen und uns Entwicklern eine Möglichkeit geben, den State einer Applikation zu lesen, ihn zu manipulieren und abzubilden.

In diesem Artikel wollen wir zu Beginn den Begriff State erklären, einen Blick auf das Projekt NgRx werfen und erläutern, welches Problem NgRx lösen kann. Anschließend werden wir die technische Seite betrachten sowie die Bestandteile von NgRx in einer Angular-Applikation mit Hilfe von Actions, Reducern und Selectors erklären. Abschließend demonstrieren wir das Konsumieren eines Stores in einer Component.

Wir schauen uns eine simple To-do-Applikation an, die ihre Items mit Hilfe von NgRx aus einem Store holt, diesen mit dem Backend synchronisiert und an der Benutzeroberfläche anzeigt.

Der vollständige Code zum Beispiel in diesem Artikel steht Ihnen auf GitHub zur Verfügung [1].

Die Beispielapplikation

Die Applikation ist eine einfache To-do-Applikation, die To-do-Items lesen und schreiben kann. Sie kann Items als erledigt markieren und via Routing auf die Detailseite eines einzelnen Items verweisen. Sie ist eine einfach gehaltene Aufgabenverwaltungsapplikation mit einem Featuremodul todo, das die Container- und Presentational-Komponenten enthält. Die Presentational Components empfangen Daten und kümmern sich darum, wie Daten angezeigt werden; die Container-Components kommunizieren mit dem Backend über Services, die über das Core-Modul eingebunden werden.

Was ist State?

Der State einer Applikation kann sich vom kleinsten Detail bis hin zu einem großen, sichtbaren Status einer Applikation erstrecken. Es kann – je nach Anwendung – alles als State betrachtet und darin gespeichert werden. Ist das Menü auf- oder zugeklappt? Welches Theme ist angewählt? Wie viele Datensätze stelle ich gerade dar? Auf welcher Seite bin ich gerade? Bin ich eingeloggt?

Kurz gesagt kann grundsätzlich jede Eigenschaft einer Applikation ein Teil des States sein.

Das Redux-Pattern

In Applikationen können sich die Eigenschaften des States ändern. Sprich: Jedes Mal, wenn der Benutzer mit meiner Applikation arbeitet, wenn sie neue Werte über ein WebSocket empfängt oder wenn aufgrund einer Reaktion auf ein Event Eigenschaften an meiner Applikation geändert werden, verändert sich auch der State der Applikation.

Das Redux-Pattern sieht vor, dass man jederzeit einen prognostizierbaren State erstellt, hält und abfragen kann. Hierbei sollte der State immer der einzige Punkt sein, auf den meine Applikation sich bezieht. Das bedeutet, der State ist die eine Single Source of Truth, also die einzige Quelle der Wahrheit. Es gibt genau einen State in der Applikation und er allein gilt als Basis und bestimmt, was die Anwendung darstellt und wie sie sich verhält.

Weiter sollte der State nur lesend verfügbar sein. Veränderungen direkt am State dürfen nicht möglich sein, sondern sollten immer per spezieller Aktion getriggert werden, die einen neuen aktualisierten State als Ergebnis liefert. Um einen neuen State zu erhalten, benötigt man also den alten State und die entsprechende Aktion (Action). Das Ergebnis ist ein neues State Object, das dann wieder entsprechendes Verhalten oder Aussehen der Applikation nach sich ziehen kann.

Änderungen am State finden also mit Actions statt. Grundsätzlich gilt: Alter State + Action = neuer State. Hierbei werden Veränderungen am State mit Funktionen bearbeitet, die bei gleicher Eingabe immer dasselbe Ergebnis haben: Pure Functions. Diese Funktionen nennt man Reducer. Sie manipulieren nicht ein existierendes State Object, sondern geben immer ein neues State Object zurück inklusive der Änderungen, die man via Action forciert hat.

Ein Reducer ist eine Funktion, die den alten State und eine Action als Parameter nimmt und ein neues State Object zurückgibt. Der State ist somit immutable:

(currentState, action) => newState

Was ist NgRx?

Das Projekt NgRx hilft uns bei der Herausforderung, State in einer Angular-Anwendung zu verwalten [2]. Es gibt uns die Möglichkeit, den Status abzufragen, ihn zu verändern und zu verwalten. NgRx ist ein Projekt, unter dem mehrere Bibliotheken verfügbar sind, die helfen, Status nach dem Redux-Pattern zu bearbeiten. NgRx abstrahiert für uns Entwickler Klassen wie Actions, Selector oder Store und hilft uns, das Redux-Pattern in einer Angular-Applikation abzubilden.

Der Store in NgRx bildet die Verknüpfung von Reducern, Actions und States. Er nimmt Actions entgegen und leitet sie an einen Reducer weiter; man kann sich auf ihm auch für Aktualisierungen an einem State registrieren.

Doch genug der Theorie, schauen wir uns das Ganze in Aktion an. Wir haben unsere Applikation in Featuremodule unterteilt. Im Folgenden schauen wir uns an, wie wir im Featuremodul Todo mit NgRx und einem Store arbeiten, den State manipulieren und Veränderungen im UI abbilden können.

Installieren der Pakete und Anlegen der Dateien und Ordner

Wir können in unserer Applikation mit npm install @ ngrx/store @ngrx/effects die erforderlichen Abhängigkeiten installieren. Das Angular CLI hat auch einen Befehl zum Scaffolding von Dateien und Ordnern eingebaut: ng add @ngrx/store. Hierbei wird etwas Code schon von vornherein generiert. Falls der nicht passt, kann man ihn mit verschiedenen Parametern anpassen [3]. In unserem Beispiel werden wir die Dateien und Ordner selbst anlegen.

Sind die Pakete installiert, können wir im Featureordner todo einen Folder store anlegen (Abb. 1).

gosebrink_ngrx_1.tif_fmt1.jpgAbb. 1: Dieser Folder verwaltet unseren kompletten Store und den Status des Featuremoduls „Todo“

Erstellen des States

Als Nächstes müssen wir den State des Featuremoduls erstellen. Der State ist nur ein JavaScript-Objekt, das alles speichern soll, was wir im State ablegen wollen. In dem Fall sind das nur unsere To-do-Items und – falls es dies gibt – das gerade ausgewählte To-do-Item. In der Datei todo.reducer.ts findet unser State seinen Platz:

export interface ReducerTodoState { items: Todo[]; selectedItem: Todo; }

Interfaces sind in TypeScript passend für Typsicherheit, somit können wir einen initialState erstellen, der uns das geschriebene Interface erfüllt und die Properties mit den Standardwerten initialisiert (Listing 1).

Listing 1

export interface ReducerTodoState { items: Todo[]; selectedItem: Todo; } export const initialState: ReducerTodoState = { items: [], selectedItem: null, };

Der State des Todo-Featuremoduls ist beliebig erweiterbar; alles, was das Todo-Modul angeht, findet hier seinen Platz.

Definieren der Actions

Wir wissen, dass wir den State nur mit Actions verändern können. Somit müssen wir im nächsten Schritt die Actions definieren, die wir an den Store applizieren wollen, um ein neues State-Objekt erhalten zu können. Hierbei sind Actions simple TypeScript-Klassen mit einem eindeutigen Identifier. Als Property können wir hier im Konstruktor eine Payload angeben. Dabei handelt es sich um Metadaten, die wir der Action mit auf dem Weg geben können, damit sie den State passend verändern kann. Eine Action, die ein bestimmtes To-do-Item abrufen soll, bekommt beispielsweise die ID dieses Items mit, damit wir dies später aus der Action extrahieren und zur HTTP-Abfrage verwenden können.

In einer erstellten Datei todo/store/todo.actions.ts definieren wir eindeutige Identifier für alle kommenden Actions in einem enum (Listing 2).

Listing 2

export enum ActionTypes { LoadAllTodos = '[Todo] Load Todos', LoadAllTodosFinished = '[Todo] L...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang