Dem Angular Framework wird häufig nachgesagt, dass es eine steile Lernkurve hat. Allerdings ist das nur teilweise richtig, getreu dem Motto “Easy to learn, hard to master”. Denn das Angular Team legt sich doch mächtig ins Zeug, um mit dem Tooling, der Dokumentation und bezüglich der gängigen Pfade einen schnellen Einstieg zu bieten.

Gerade auch Entwickler:innen aus dem Java- oder C#-Umfeld könnten sich dahingehend wohlfühlen, dass Angular auf klassenbasierte und mit Dekoratoren notierte Komponenten setzt, die in einem Modul-System strukturiert werden können. Ein Aspekt, auf den wir in Angular schnell stoßen, ist das Thema Reaktivität und die Bibliothek RxJS. Denn selbst bei einfachen HTTP-Operationen mit einer Anfrage und einer Antwort setzt Angular auf das allgemeingültige Konzept der Datenströme.

Der gewünschte Weg, mit asynchronen Daten umzugehen, ist der Einsatz von RxJS Observables. Diese sind zwar äußerst vielfältig einsetzbar und gerade durch die bereits vorhandenen Operatoren sehr mächtig, aber erfordern auch eine Menge Wissen und Erfahrung, um damit produktiv und korrekt zu arbeiten.

Erste Oberflächen mit Angular zu bauen, ist nicht allzu schwierig. Aber auch bei komplexen Anwendungen eine gute Performance zu gewährleisten (z.B. durch den Einsatz des OnPush Mechanismus der Change Detection) hat eine gewisse Komplexität und erfordert oft den sauberen Einsatz dieser Observables.

Nicht zuletzt, um diese Komplexitätshürde zu senken, hat das Angular Team ein neues Konzept namens Signals eingeführt, welches mit der aktuellsten Version 17 von der Vorabversion in den stabilen Zustand übergeht, d.h. es lässt sich ohne Bedenken in Produktivumgebungen einsetzen. Diesen Ansatz wollen wir uns in diesem Artikel ansehen.

Anwendungsbeispiel

Das vorherrschende Gefühl bei der Anwendung der Signals erinnert stark an die Composition API von Vue. Falls Sie hiermit Erfahrung haben, kann es gut sein, dass Ihnen die Patterns bekannt vorkommen.

Um später einen Vergleich ziehen zu können, schreiben wir eine einfache Beispielapplikation, die ich in der Vergangenheit schon im Vue-Umfeld genutzt habe: Die sogenannte Lamafarm zeigt eine Übersicht aller Lamas, die auf der Farm leben. Über eine Suchleiste können wir diese anhand des Namens filtern. Ein einzelnes Lama lässt sich per Klick zum Spazierengehen ausführen und mit den Pfeiltasten auf der Wiese hin- und herbewegen.

Angular Signals Abbildung 1 - Anwendungsbeispiele

Anhand dieses Anwendungsbeispiels werden wir uns im Folgenden die wesentlichen Bestandteile der Angular Signals ansehen. Wir setzen dabei voraus, dass wir mit dem bisherigen Standard-Weg, Angular Applikationen zu schreiben, vertraut sind. Zumindest sollten Sie als Leser:in Angular’s Tour of Heroes schonmal durchlaufen haben, um ein grundlegendes Gefühl dafür zu haben.

Große Auswahl an günstigen Domain-Endungen – schon ab 0,08 € /Monat
Jetzt Domain-Check starten

Einstieg in Angular Signals

Im Grunde genommen ist ein Signal ein Wrapper um einen Wert – wobei der Wert ein Objekt oder primitiver Datentyp sein kann. Dadurch wird der Umgang mit primitiven Typen etwas unhandlicher, folgt aber dem gleichen Konzept.

Dieses Wrapper-Objekt dient dazu, dass es bei Änderungen des Wertes andere Interessenten informieren kann (daher vielleicht der Name Signals: Wir signalisieren, dass jemand den Wert geändert hat), wodurch Angular zu jeder Zeit weiß, wann Render-Updates durchzuführen sind.

Signals Grundlagen

Das Signal Wrapper-Objekt wird über die Funktion signal() erzeugt und kann dabei einen Initialwert erhalten (Code-Beispiel 1). Wir greifen mit einer Getter-Funktion (lesender Zugriff, Code-Beispiel 2) oder Setter-Funktion (schreibender Zugriff, Code-Beispiel 3) auf ein Signal zu.

Es ist dabei unerheblich, ob innerhalb des Templates oder dem Komponenten-Code mit dem Signal gearbeitet wird. Ein schreibender Zugriff kann unterbunden sein, dann ist das Signal readonly.

Soll der bestehende Wert erneuert werden, so kann statt dem Aufruf des Getters, gefolgt vom Setter, auch die update-Funktion genutzt werden (Code-Beispiel 4). Diese liefert den existierenden Wert über eine Funktion, die wiederum das Update durchführt.

Wie bereits erwähnt, können Signals auch ganze Objekte enthalten. Dabei ist zu beachten, dass Änderungen innerhalb des Objekts nicht erkannt werden. Ich persönlich begrüße es grundsätzlich, mit sogenannten immutable Objekten zu arbeiten: Objektinhalte werden nicht geändert, sondern Wertänderungen erzeugen eine Kopie des bestehenden Objekts mit anderen Inhalten. Alternativ ließe sich auch die spezielle mutate-Funktion nutzen.

Für welchen Weg man sich entscheidet, ist in vielen Fällen Geschmackssache (Immutability kann den Programmfluss vereinfachen) oder eine Frage bezüglich der Optimierung von Performance (Kopien können teuer sein).

Deklarative Programmiermuster

Den größten Vorteil bieten Signals meiner Meinung nach in der Nutzung der computed-Signals. Diese Variante eines Signals mit nur lesendem Zugriff erlaubt es, Programmcode eher deklarativ statt imperativ zu schreiben.

Deklarativer Code liest sich wie eine Beschreibung der genutzten Elemente, imperativer Code hingegen wie eine Abfolge von Befehlen. Ersteres kann oftmals einfacher zu verstehen sein. Der Pseudocode aus Code-Beispiel 5 kann nur umgesetzt werden, wenn die dort verwendeten Variablen alle von ihnen abhängigen anderen Elemente über Änderungen informieren.

Ist dies nicht der Fall, wären wir gezwungen, entsprechende Update-Methoden zur richtigen Zeit aufzurufen (Pseudocode-Beispiel 6).

Wollten wir solch deklarative Logik in Angular ohne Signals umsetzen, so konnten wir bisher normale Getter oder Methoden der Komponentenklasse nutzen (Code-Beispiel 7). Zwar ist die Lösung sehr schlank und gut lesbar, jedoch hat sie den großen Nachteil, dass die Change Detection von Angular nicht weiß, wann Änderungen stattfinden und der Wert abgerufen werden muss. Demzufolge werden diese Funktionen in jedem Change Detection Zyklus aufgerufen, auch bei Interaktion mit Elementen, die damit gar nichts zu tun haben (Screencast 1). Bei komplexen Anwendungen kann dies schnell zu vielen überflüssigen Aufrufen und großen Performanceeinbußen führen.

Angular Signals Abbildung 2 - Deklarative Programmiermuster

Setzen wir RxJS ein und bauen unsere Anwendung sowieso um das Streaming-Pattern herum, so können wir dies zwar direkt damit umsetzen, zum Beispiel durch den Einsatz von Subjects und gepipte Observables. Wie bereits erwähnt, müssen wir dann aber die RxJS-Patterns ausreichend gut beherrschen.

Weiterhin muss dabei relativ viel Boilerplate-Code (notwendiger Infrastruktur-Code, der nichts zur eigentlichen fachlichen Funktionalität beiträgt) mit RxJS-Operatoren geschrieben werden (Code-Beispiel 8). Schließlich können wir aber erkennen, dass es hier nicht zu zahlreichen überflüssigen Neuberechnungen kommt.

Angular Signals Abbildung 3 - Deklarative Programmiermuster

Angular Signals bieten ein Stück weit das Beste aus beiden Welten und einen guten Mittelweg. Der Code wirkt fast so schlank wie bei der Getter-Variante, besitzt aber Notifizierungsfunktionen wie bei den Observables (mit dem Kompromiss, nicht das gesamte Feature-Set der RxJS-Operatoren zur Verfügung zu haben) (Code-Beispiel 9).

Angular stellt dabei sicher, dass die computed-Funktion nur ausgewertet wird, sofern das darin enthaltene Signal eine Wertänderung erfährt (im Gegensatz zur klassischen Getter-Klassenmethode, die unabhängig immer in jedem Zyklus aufgerufen wird). Mehrfache Zugriffe auf das computed-Signal liefern zudem den bereits berechneten Wert, den Angular dafür zwischenspeichert.

Angular Signals Abbildung 4 - Deklarative Programmiermuster

Seiteneffekte

Können wir mit den computed-Signals in Abhängigkeit von anderen Signals Werte berechnen, so eignen sich die sogenannten Effects dazu, auf Änderungen von Signals zu reagieren und – wie ihr Name schon sagt – Seiteneffekte auszulösen. Dadurch, dass die Entwicklung mit Signals grundsätzlich deklarativer ist, werden Effects zwar weniger häufig benötigt, sie können aber gut eingesetzt werden für sogenannte Cross-cutting concerns, wie beispielsweise Logging (Code-Beispiel 10).

Bestandscode und Anbindung an RxJS

Sollte nun die gesamte Codebase in Richtung Angular Signals umgebaut werden? Es ist möglich, aber nicht unbedingt empfehlenswert. Alle bisherigen Implementierungsmuster sind weiterhin verwendbar, und Signals lassen sich schrittweise und nur an ausgewählten Stellen einsetzen.

Darüber hinaus gibt es mit @angular/core/rxjs-interop ein Paket, welches die Verwebung von RxJS- und Signal-Code erlaubt. So können selbst umfangreich auf Observables ausgelegte Applikationen an gezielten Stellen durch Signals ergänzt werden und umgekehrt. Dies geschieht dabei in der jeweiligen Richtung durch die Methoden toSignal bzw. toObservable, die ein Observable in ein Signal umwandelt und umgekehrt. Da beide Technologien ein Push-Pattern einsetzen, ist dies ohne eigentlichen Funktionalitätsverlust möglich.

Kurzer Vergleich Vue Composition API

Wie zu Beginn bereits angedeutet, ähnelt die Syntax der Angular Signals der der Vue Composition API. Im Code-Beispiel 11 ist die Ähnlichkeit in der Gegenüberstellung nicht zu übersehen. Auch wenn beides im Kern technisch auf unterschiedlichem Wege umgesetzt wird, ist das mentale Modell dahinter das gleiche.

Komfortable Reaktivität mit Angular Signals – Fazit und Ausblick

Mir persönlich gefallen die Angular Signals sehr, denn sie vereinen die Einfachheit, deklarativen, leicht lesbaren und nachvollziehbaren Code zu schreiben mit der selektiven Reaktivität der Observables. Wir können nun auch in Angular sehr performanten Code schreiben, ohne gleich ein RxJS-Profi zu sein. Dass Signals darüber hinaus stückweise in den bestehenden Code integriert werden können, ist ein großer Pluspunkt und ermutigt, dieses Feature auch einzusetzen. Da Signals mit Version 17 jetzt als stabil gelten, lassen sie sich auch schon im Produktivcode anwenden.

Künftig möchte das Angular Team das Ökosystem rund um die Signals erweitern. Unter anderem sollen Komponenten über ein Decorator-Attribut signals: true als Signal Component gekennzeichnet werden, welches zusätzliche Komfort-Funktionalitäten bietet, wie z.B. die Handhabung von @Inputs oder @Outputs als Signals. Die Verfügbarkeit von Signals eröffnet Angular weiterhin den Weg hin zu einem Framework, das bei der Change Detection nicht mehr von Zone.js abhängig ist. Es bleibt spannend.

Der vollständige Quellcode zu diesem Artikel findet sich auf GitHub.

Titelmotiv: Photo by Pakata Goh on Unsplash

David Würfel

Große Auswahl an günstigen Domain-Endungen – schon ab 0,08 € /Monat
Jetzt Domain-Check starten