April 11, 2024

Angular Signals Teil 2 – Wie kombiniert man Angular Signals und RxJS

Go here for the english version of this blog post.

Angular 17: Signals + RxJS = Perfekte Ergänzung

Im ersten Beitrag dieser Artikelserie, Angular Signals Teil 1 – How-to Guide für Angular Signals, haben wir zusammengefasst was Angular Signals sind und wie man sie verwendet. Jetzt wollen wir uns anschauen warum Signals kein Ersatz für RxJS sind und wie man beide Technologien am besten kombiniert.

Angular Signals Part 2

Wie bereits festgehalten, sind Signals besonders gut dafür geeignet synchrone Zustände in der Anwendung zu verwalten. Damit übernehmen sie aber eine Aufgabe die bisher von RxJS behandelt wurde. Die Frage ist also, ob Signals RxJS gänzlich ablösen werden?

Die Antwort darauf ist: Nein.

Um zu verstehen, warum RxJS mit der Verwendung von Signals nicht weniger wichtig wird, muss man die Anwendungszwecke der Technologien und ihre Stärken und Schwächen kennen. Genau dies, werden wir in diesem Beitrag machen.

Im Folgenden Blog erfährst Du wie Du Angular Signals und RxJS zusammen verwendest. Wenn Du mehr über Signals und RxJS im Detail lernen und alle Tipps und Tricks erfahren möchtest, besuche einen unserer Angular-Kurse:

Stärken und Schwächen der Technologien

Um die passende Technologie für den entsprechenden Anwendungszweck zu wählen, muss man über ihre Vor- und Nachteile Bescheid wissen. Welche Stärken und Schwächen Signals und RxJS haben schauen wir uns als nächstes an.

RxJS

Der folgende Abschnitt ist natürlich keine vollständige Analyse oder Einführung zu RxJS. Er soll nur einen Überblick geben, damit eine Einschätzung und Abgrenzung bezüglich Angular Signals gemacht werden kann. Wenn Du noch mehr über RxJS wissen möchtest kannst du hier einen Blick in ihre Dokumentation werfen.

Es gibt zwei Aufgabenbereiche die RxJS abdeckt. Zum einen die Verwaltung und Koordination von asynchronen Events, zum anderen die reaktive Programmierung. Mit dem Observable-Pattern kann RxJS effizient asynchrone Events und Operationen, als Datenströme handhaben.

Die Vielzahl von Operatoren, wie map oder filter, die zur Verarbeitung und Umwandlung der Observables zur Verfügung stehen, ist eine große Stärke von RxJS. Dadurch können komplexe Aufgaben, z.B. das Koordinieren von mehreren Ereignissen oder das Kombinieren von Datenströmen, simpel und leserlich gelöst werden. Mit dem HTTP-Client bekommen wir in Angular einen Service zur Verfügung gestellt, der uns direkt eine Arbeit mit Observables ermöglicht. Observables ermöglichen es uns die HTTP-Ereignisse, wie success, error und completion, zu behandeln und zu verarbeiten.

RxJS war, mit Bibliotheken wie Tydux oder NgRx, das bisherige Go-To für globales State Management. An diesem Punkt können uns nun Signals das Leben deutlich erleichtern.

Angular Signals

Im Gegensatz zu RxJS haben Signals eine eingeschränkte Funktionalität, was das Reagieren auf Events angeht. Da man nur die effect() und computed() Funktionen nutzen kann, um auf Änderungen in anderen Signals zu reagieren, und nicht so viele und umfangreiche Operatoren zur Hand hat, können gerade komplexere Aufgaben zu viel und unleserlichem Code führen. Signals haben beispielsweise keine klassischen und bekannten Operatoren wie catchError oder switchMap, die dafür zuständig sind, Anfragen zu einem bestimmten Zeitpunkt abzubrechen, wenn eine neue Anfrage mit neuen Daten kommt.

Signals zielen nicht darauf ab RxJS gänzlich zu ersetzen, sondern die reaktive Programmierung zu vereinfachen.
Ein gutes Beispiel sind die folgenden Code-Blöcke, die beide die gleiche Funktionalität darstellen. Wenn sich die Variablen firstName oder lastName ändern, soll die Variable fullName aktualisiert werden.

Der erste Block zeigt wie RxJS diese Aufgabe mit dem fullName$ Observable löst. Wir erzeugen mit combineLatest ein neues Observable, dass unsere beiden Observable Quellen kombiniert. Dabei müssen wir daran denken, dass combineLatest erst Werte produziert, wenn sowohl firstName$ und lastName$ mindestens einmal einen Wert geliefert haben. Zur Verwendung des eigentlichen Wertes von fullName$ müssen wir an dem Observable subscriben. Dann müssen wir uns auch über das Beenden der Subscription kümmern.

Mit Signals, unter Verwendung der computed() Funktion, sieht das Ganze anders aus. Auf den Wert eines Signals greifen wir mit der Verwendung der runden Klammern zu. In der computed() Funktion werden die Abhängigkeiten an den Inhalt der Signals firstName und lastName automatisch erfasst, nur dadurch, dass wir deren Inhalte verwendet haben. Auch müssen wir keine Subscription mehr verwalten. Und zu guter Letzt haben Signals immer einen initialen Wert bei ihrer Erzeugung.

RxJS ist ein leistungsfähiges und vielseitiges Werkzeug, mit vielen mächtigen Operatoren. Uns muss das Verhalten der einzelnen Operatoren klar sein, um sicher mit RxJS arbeiten zu können. Zusätzlich müssen wir uns um das Erzeugen und Kombinieren von Observables und deren Subscriptions kümmern. Signals auf der anderen Seite sind simpler in der Verwendung. Wir brauchen keine Subscriptions und Abhängigkeiten zu managen. Die Ausführung von Signals ist allerdings synchron und kann daher nur die synchronen Teile von RxJs ersetzen und dabei das Programmieren und das State Management vereinfachen.

Wie kombiniert man die beiden Ansätze?

Da wir nun festgehalten haben, dass RxJS weiterhin wichtig und relevant ist, bleibt die Frage wie man die beiden Technologien zusammen verwendet. Im Prinzip genau so wie wir es oben festgehalten haben. Für synchrone Vorgänge, wie das State Management, verwendet man Signals und alle asynchronen Operationen, wie Events und Anfragen an die Datenbank, sollten mit RxJS behandelt werden.

Es gibt zwei Funktionen, die dafür sorgen, dass genau dieses Zusammenspiel reibungslos funktioniert.

toSignal()

Mit der toSignal() Funktion kann man ein readonly Signal erzeugen, dass den Wert eines Observables verfolgt.
In unserem Beispiel haben wir ein Observable movies$, welches auf die Antwort eines HTTP-Requests wartet, in unserem Fall eine Liste von Filmen. Das Signal movies wird mit toSignal() an movies$ geknüpft und sobald die Antwort da ist, und das Observable die erhaltenen Filme ausgibt, aktualisiert sich auch das movies Signal.

Dieses Signal können wir dann im Template benutzen um die Filme anzuzeigen. Dadurch brauchen wir im Template auch keine async pipe mehr.

toSignal() subscribed also von selbst an das entsprechende Observable und unsubscribed auch automatisch, wenn die Komponente oder der Service in der der Aufruf gemacht wurde zerstört wird.

Bei einer Sache muss man aber noch aufpassen. Da toSignal() sofort an das Observable subscribed, kann es zu verschiedenen Seiteneffekten kommen. Wenn an dem Observable z.B. ein HTTP-Request hängt, wird dieser Aufruf sofort ausgeführt, unabhängig davon, ob es gerade eine Komponente gibt die die Daten haben möchte. Dies kann besonders in Shared Services schwierig sein.
Speicherlecks (Memory Leaks) sind ebenfalls etwas, das man im Hinterkopf behalten sollte. toSignal() kann nur in einem Injection Kontext oder mit einem Injector aufgerufen werden. Das heißt, wenn der Injector, und das damit verknüpfte Signal, über den Lebenszyklus der Komponente hinaus existieren, kann es zu Speicherlecks kommen. Insbesondere dann, wenn toSignal() in root Services oder root Komponenten aufgerufen wird.

toObservable()

Umgekehrt zu toSignal() erzeugt man mit toObservable() ein Observable, dass den Wert eines Signals verfolgt. Dadurch kann man z.B. einen weiteren HTTP-Request auslösen und weitere Daten nachladen.
In unserem Beispiel haben wir ein Signal selectedMovie, dass den Film speichert den der Nutzer gerade angeklickt hat. Mit toObservable() können wir jedes mal, wenn selectedMovie sich ändert reagieren und in der Observable-Pipeline einen weiteren HTTP-Request machen, mit dem wir die Charaktere des Films abfragen. Die entsprechende Antwort fangen wir dann mit dem movieCharacters$ Observable ab.

Somit können wir auf synchrone Zustandsänderungen mit asynchronen Operationen reagieren. Das movieCharacter$ Observable kann jetzt auch wieder mit einem Signal für Templates verfügbar gemacht werden.
Mit der toSignal() und toObservable() Funktion lassen sich asynchrone Vorgänge sehr gut im Service abhandeln und kapseln. Nach außen sind dann nur Signals zugänglich, die dem synchronen State Management dienen.

ngxtension

ngxtension ist eine Bibliothek mit Hilfsfunktionen für Angular, welche das Entwickeln mit Angular einfacher machen sollen. Mittlerweile gibt es auch schon mehr als zehn Funktionen, die die Arbeit mit Signals erleichtern. Wir möchten jetzt noch zwei dieser Funktionen vorstellen, die wir am praktischsten finden.

toLazySignal()

Die Funktion toLazySignal() funktioniert ähnlich wie die originale toSignal() Funktion, mit der Ausnahme, dass nicht sofort eine Subscription erzeugt wird. toLazySignal() setzt also genau da an, wo wir oben bei toSignal() ein Problem gesehen haben. Erst wenn das Signal das erste Mal gelesen wird, subscribed Angular an das entsprechende Observable.

An diesem Code-Schnipsel kann man sehen, dass die Funktionen genau gleich verwendet werden. Im eingebundenen StackBlitz-Projekt haben wir die beiden Funktionen in einer Komponente auch nochmal gegenüber gestellt, damit Du die Subscriptions selber ausprobieren kannst.

connect()

Die connect() Funktion verknüpft ein Signal mit einem Observable. Das klingt jetzt erstmal nicht anders als die toSignal() Funktion, aber während man bei toSignal() nur ein readonly Signal erhält, liefert connect() ein writeable Signal.

Wenn man also den Wert eines Signals basierend auf einem Observable setzen will, kann man das wie im folgenden Beispiel machen:

Das Signal signalName wird dabei an das valueChanges Observable der FormControl geknüpft. Immer wenn sich das Formular updated, weil der eingetragene Wert geändert wird, aktualisiert sich auch das Signal.

connect() hat aber nicht nur diesen einen Anwendungszweck. Es ist ebenfalls möglich zwei Signals miteinander zu verknüpfen, wobei man auch wieder ein writeable Signal erhält, und connect() ermöglicht auch, dass eine beliebige Anzahl von Streams mit nur einem Signal verbunden werden kann. Es gibt also eine Vielzahl von Anwendungsfällen, wo die connect() Funktion einem die Arbeit erleichtert.

Erfahrung ist der beste Lehrer: Hands-On mit Signals und RxJS

Bist Du bereit, die Theorie in die Praxis umzusetzen? In diesem interaktiven StackBlitz-Beispiel haben wir eine Beispielanwendung erstellt, die die Grundkonzepte von RxJS und Signals aufgreift und alle vorgestellten Beispiele und Funktionen beinhaltet. Tauche ein und experimentiere, um Signals und RxJS in Aktion zu erleben!

Und jetzt?

Angular Signals sind immer noch relativ neu und es wird sich zeigen wie es mit Signals, auch in weiteren Angular Versionen, noch weitergeht. Da wir alle jeden Tag dazulernen, und wir euch das Arbeiten mit Signals noch leichter machen möchten, wollen wir im nächsten Artikel dieser Reihe Signal Inputs genauer betrachten und sie mit dem klassischen @Input-Dekorator vergleichen.

Terminübersicht der nächsten Angular Schulungen

Related Posts

theCodeCampus Autorin Anne Naumann

Anne Naumann
Developer at thecodecampus </>


Leave a Reply

Add code to your comment in Markdown syntax.
Like this:
`inline example`

```
code block
example
```

Your email address will not be published.