Die Angular APIs bringen die lang ersehnte Reaktivität inklusive Input-Bindings direkt ins Framework. Dadurch verändern sich viele Bereiche der Enterprise Web Frontend Plattform. Die Synchronisation zwischen JavaScript Zustand und gerendertem Browser-Inhalt wird performanter und das Framework im gesamten einfacher zu erlernen und auch leichtgewichtiger in der Verarbeitung. Dieser neue Stil verlangt allerdings weitgehendst manuelle Refactorings, die in großen Codebases auch entsprechend eingeplant werden müssen. Das Angular Team stellt allerdings auch Abwärtskompatibilität bereit und liefert unterstützende Code-Generatoren.
Die wichtigsten Angular Signal APIs sind nun für den produktiven Einsatz freigegeben (stable) und daher sollte jedes Entwicklerteam spätestens ab jetzt neue Features mit Signals ausstatten. Das Angular-Team hat in einem Evaluierungsprozess mehrere Konzepte bewertet – auch RxJS und JavaScript Proxies wurden für deren Eignung als neues „Reactive Primitive“ des Frameworks analysiert – und das Signals Konzept hat die größte Übereinstimmung mit deren Zielsetzungen gezeigt. Was können Signals also so besonders gut, sodass sie sich sogar gegen RxJS durchsetzen konnten, das seit Angular Version 2 als Drittanbieter Bibliothek verpflichtend in jedem Angular Projekt mitinstalliert wird? Signals sind mit Sicherheit nicht das Universalwerkzeug wie RxJS samt all seiner Operatoren, aber sie glänzen in Teilbereichen, die speziell für den Synchronisationsprozess zwischen Browser-DOM und JavaScript-Zustand verantwortlich sind. Zudem ist das (reaktive) Abrufen von Werten im Vergleich zu RxJS deutlich komfortabler und vor allem wird der Wert immer synchron – also sofort – zurückgegeben. Kein subscribe/unsubscribe, kein async/await – einfach ein Funktionsaufruf und der im Signal verwaltete Wert steht schon typkonform bereit. Ein wichtiges Kriterium ist auch, dass man das Framework einfacher machen will. Einfacher nicht im Sinne von weniger bereitgestellten Features, aber hinsichtlich der Erlernbarkeit für neue Developer, die vor der Entscheidung stehen, auf welches Frontend-Framework sie sich in Ihrer künftigen Karriere fokussieren wollen. Dies ist für jeden und jede von uns wichtig – denn nur wenn ausreichend ausgebildete Angular Entwickler am Markt verfügbar sind, werden sich Unternehmen weiterhin für diese Plattform entscheiden. Bliebe Angular für alle Ewigkeit bei den bestehenden APIs, während andere Frameworks mutig neue Konzepte implementieren und etablieren, würde sich jeder Entscheidungsträger irgendwann die Frage stellen, ob Angular noch immer die beste Wahl für die eigenen Projekte ist. Das Angular-Team geht eben diesen mutigen Weg, allerdings auch einen, der es ermöglicht, all diese Innovation möglichst sanft in die eigene Codebase einbauen zu können und uns damit vor vergleichsweise sehr wenige Breaking Changes stellt. Für jeden Breaking Change wird – wenn technisch möglich – ein Angular CLI Schematics Generator angeboten, der die notwendigen Refactorings entweder vollautomatisch umsetzen kann oder bei der Migration zumindest Unterstützung anbietet. Im Fall von Signals gibt es mehrere unterstützende Generatoren, die beim manuellen Refactoring helfen [1], [2], [3]. Ein alternatives Beispiel für Update Schematics, die fast zu 100 Prozent automatisch umbauen können, sind jene für den neuen Control Flow (@if, @for, @switch) [4].
„Zahlreiche Vertreter der Angular Community wünschen sich schon sehr lange eine tiefere reaktive Framework-Unterstützung.“
Warum Signals?
Die neue reaktive Reise hat mit Angular 16 begonnen und ist nun, mit Version 21, schon sehr weit fortgeschritten. Signals sind allerdings kein Konzept das vom Angular Team erfunden wurde, sondern sie wurden vom „CEO of Signals“, dem Entwickler von SolidJS, Ryan Carniato, 2016 in die moderne Single-Page-Application-Welt gebracht [5]. Die früheren Wurzeln gehen zu Xerox und Libraries wie Knockout zurück. Heute verwenden zahlreiche Frontend Bibliotheken und Frameworks, wie z. B. Vue, Preact und Qwik Signal Konzepte. Mittlerweile ist auch ein offizielles TC39 Proposal [6] veröffentlicht worden, mit dem das Ziel verfolgt wird, Signals zu einem EcmaScript Sprachstandard zu machen. Das Angular Team war hier beteiligt und hat auch die Referenzimplementierung bereitgestellt [7].
Zahlreiche Vertreter der Angular Community wünschen sich schon sehr lange eine tiefere reaktive Framework-Unterstützung. Ein Teil dieser Initiative war die Etablierung des NgRx-Namespaces, der genau dieses Ziel verfolgt und anfangs vor allem für seine Event-basierte, reaktive State Management Implementierung nach dem Redux-Pattern bekannt wurde. NgRx und auch andere Drittanbieter Bibliotheken stoßen allerdings spätestens dann an Ihre Grenzen, wenn es um tiefere Framework-Integration wie Change Detection (wird mittlerweile „Synchronization“ genannt) oder Data Bindings geht. Letzteres erfordert auch Compilerintegration, damit die Rendering Engine Ivy korrekt angebunden werden kann. Aus diesen Gründen war es notwendig, dass das Angular Team selbst die relevanten reaktiven APIs direkt in den Framework Code einbindet.
Aus dem internen Evaluierungsprozess ist das Signals Konzept als Sieger hervor gegangen, weil es die gesetzten Ziele am besten umsetzen konnte:
- Werte werden per Funktionsaufruf sofort – also synchron – zurückgegeben
- Signals können in bestimmen Fällen abhängigen Quellcode benachrichtigen und als Konsequenz kann dieser erneut ausgeführt werden
- um abgeleiteten Zustand zu ermitteln
- Side-Effects auszulösen
- das UI zu aktualisieren
- Werteupdates, die zum selben Resultat führen, lösen keine Benachrichtigung aus
- bei Objekten wird die Referenz verglichen
- Immutable Updates sind hier also wichtig: wird eine Property eines Objekts verändert, muss auch eine neue Objektreferenz erzeugt werden
Als die ersten Signal APIs mit Version 16 veröffentlicht wurden, waren auch noch mutable Updates für schreibbare Signals erlaubt. Dies wurde mit Version 17 – sinnvollerweise – korrigiert. Einen Wert zunächst durch Objektmutation zu verändern hat einen Preis, den man später durch einen aufwendigen Vergleich, potenziell auf mehreren verschachtelten Objektebenen, hinsichtlich beeinträchtigter Runtime-Perfomance bezahlen muss. Zudem hat die Version 17 grundlegende APIs wie signal() und computed() sowie andere relevante Signal APIs und Typen aus dem Core Paket als stabil definiert. Andere wichtige Features wie effect() und toSignal(), toObservable() aus dem RxJS-Interop API wurden jedoch im Developer Preview Status belassen, da die interne Einplanung der Effects mehrmals angepasst werden musste. Auch diese APIs wurden nun mit Version 20 in den stabilen Zustand befördert. Dazwischen wurden mit Feature-Releases der Version 17 – endlich – Signal-basierte, reaktive Bindings Realität: input() für readonly Property-Bindings und model() für schreibbares Two-Way-Binding. Ergänzend wurde mit output() auch noch ein dekoratorloses Event-Binding API veröffentlicht, das aber keine Signals für die eventbasierte Kommunikation verwendet, sondern weiterhin einen EventEmitter. Alle diese APIs wurden mit Version 19 stabil. Dieses Major Release brachte uns auch noch das linkedSignal() und die Familie der Resource APIs, um Signals als Trigger für asynchrone Prozesse zu verwenden, die nicht zwingend mit RxJS implementiert sein müssen, aber erneut damit kompatibel sind. Das linkedSignal() wurde bereits mit Version 20 stabil, während die Resource APIs weiterhin experimentell sind. All diese Features werden im Rahmen dieses Artikels näher betrachtet.
„Das Signal kann einfach per Funktionsaufruf gelesen werden – ohne AsyncPipe.“
Grundlegende APIs
Starten wir mit jenem Signal API, mit dem man üblicherweise zuerst in Kontakt kommt – das Writable Signal, das mit der Funktion signal() erstellt wird. Diese und viele andere Features können direkt aus dem @angular/core Paket importiert werden. Das Angular Team hat sich – im Gegensatz zu RxJS – diesmal dafür entschieden, keine Drittanbieter Library zu verwenden, sondern den Code selbst zu implementieren, damit die rückwärtskompatible Unterstützung mit der „alten Welt“, vor der Signals-Ära, bestmöglich gewährleistet werden kann. signal() erzeugt ein Objekt, das aber gleichzeitig auch eine Funktion ist (ja, auch das kann JavaScript). Der Funktionsaufruf retourniert den aktuell verwalteten Wert sofort. Über die Methoden set() und update() kann der Wert verändert werden, was wie erwähnt, immer immutable erfolgen muss. set() überschreibt den vorhanden Wert mit einem neu Übergebenen, während update() eine Lambda-Expression entgegennimmt, die auf den aktuellen Wert zugreifen kann und deren Return-Value als neuer Signal-Wert übernommen wird. signal() wird initial ein Wert übergeben, der per Type-Inference festlegt, welchen Typ das Signal künftig verwaltet. Wird kein Wert übergeben und stattdessen der Typ über die Spitzen Klammern angeführt, ist auch undefined Teil der erlaubten Typsignatur:
const firstname = signal<string>(); // WritableSignal<string | undefined>
Der Wert des Signals kann einfach per Funktionsaufruf, sowohl im TypeScript-Code als auch im Template – und zwar ganz ohne AsyncPipe – gelesen werden:
<p>Firstname: {{ firstname() }}</p>
Mit computed() wird abgeleiteter Zustand als Signal (readonly) definiert. Dieser Signaltyp bietet keine set() oder update() Methode an, kann aber stattdessen Signals lesen und wird dann zuverlässig benachrichtigt, wenn sich die abhängigen Signals verändern. computed() wird eine Lambda-Expression übergeben, die einen Return-Value zurückgibt, der den abgeleiteten Zustand ermittelt. Allerdings sind Computed Signals lazy – werden Sie von niemandem in der Codebase gelesen, läuft die Lambda-Expression auch niemals. Auch ein indirekter Leseprozess über ein oder mehrere weitere abgeleitete Signals ist aber ausreichend, solange am Ende der Kette ein Binding im Template erfolgt oder ein effect() direkt oder indirekt (z. B. toObservable(), resource(), rxMethod() [8]) zur Anwendung kommt.
Side-Effects können über effect() definiert werden. Dies erfolgt, wie bei computed, über eine übergebene Lambda-Expression, aber mit dem Unterschied, dass hier kein Wert zurückgegeben wird. Es werden aber auch hier Signals gelesen und der Effect wird für eine neue – immer asynchrone – Ausführung eingeplant, wenn zumindest eines der gelesenen Signals eine Wertänderung aussendet.
„Warum verwendet man nicht immer model()?“
Kommunikation zwischen Components
In modernen Angular Frontends werden üblicherweise zwei Komponentenkategorien verwendet. Smart Components sind Container, die in der Routing Konfiguration verwendet werden und Feature-spezifische Services wie z. B. Data Access oder State Management Services injecten. Die zweite Kategorie sind UI (oder Presentational bzw. Dump) Components, die sich um das Detail-Rendering kümmern, oft wiederverwendbar sind und ihren Zustand lediglich über Data Bindings erhalten. Eben dieses Data Binding ist nun auch mit Signals realisierbar und ermöglicht das lang ersehnte Feature der reaktiven Inputs. Diese UI Components werden von einer übergeordneten Smart Component in deren Template verwendet und dadurch am Bildschrim angeordnet. Das Schöne an der Signal Integration für Bindings ist, dass das Angular Team auch hier wieder die Kompatibilität mit bisherigen Implementierungen klar im Fokus hat. Dadurch ist es möglich, dass eine Smart Component refactort wird, ohne die UI Component verändern zu müssen oder umgekehrt. Die gebundenen Daten kommen zwar als Signal in der Kind-Komponente an, müssen aber nicht zwingend im Template der Parent Component als Signal gebunden werden. Die Smart Component kann einen ganz normalen Wert binden und in der UI Component wird der Wert über ein Input Signal bereitgestellt. Dies kann mit der Funktion input() erreicht werden, die ein nur lesbares Signal erzeugt. Änderungen können in diesem Fall also nur per Binding durchgeführt werden und nicht innerhalb der Komponente, die den Input definiert, selbst. Dieser Einschränkung muss beim Refactoring beachtet werden und macht gegebenenfalls eine Änderung der Implementierung notwendig.
readonly firstname = input.required<string>();
Der Beispielcode zeigt einen Signal-basierten Input vom Typ string, der in diesem Fall als required definiert ist und daher von der Parent Component zwingend gesetzt werden muss. Erfolgt das nicht, zeigt die IDE eine rote Fehler-Unterwellung im Template und auch der Compiler wirft den Fehler aus. Inputs können mittlerweile auch durch den Router gesetzt werden, sofern dieser entsprechend konfiguriert ist:
provideRouter([{ path: 'edit/:id', component: EditComponent }], withComponentInputBinding())
Damit versucht der Router nun, seinen Zustand an gleichnamige Inputs der referenzierten Komponente zu binden. Dies betrifft dynamische Pfad-, Query- und Matrix-Parameter genauso wie Zustand aus dem statischen data-Objekt der Route oder dynamischen Daten, die per Router Resolver abgeholt werden [9].
Im nachfolgenden Beispiel wird der Input mit einem Initialwert definiert, der im Laufe des Lebenszyklus der Component aktualisiert werden kann. Im gegebenen Fall wird er Zustand des dynamischen Routen Parameter id an den gleichnamigen Component Input gebunden. Da der Parameter in diesem Fall aus der URL ermittelt wird, liegt er als String vor und kann über eine simple Transform-Funktion, die das Framework bereitstellt, in einen Number-Wert umgewandelt werden:
readonly id = input(0, { transform: numberAttribute });
Ein required Input darf keinen Initialwert definieren, da vorausgesetzt wird, dass die Parent Component diesen setzen muss. Trotzdem ist es wichtig zu beachten, dass der initial gebundene Wert zum Zeitpunkt der Komponenteninstanzierung – also bei Property-Zuweisung oder im Konstruktor – noch nicht bereitsteht. Will man diesen lesen, benötigt man, wie bisher, eine ngOnInit-Implementierung. Alternativ kann eine effect()-Definition im Konstruktor verwendet werden, der den Input liest. Für den Effect wird garantiert, dass dessen erstmalige Ausführung erst dann erfolgt, wenn der Input gesetzt wurde.
constructor() { effect(() => console.log(this.firstname()); }
Ein weiteres, neues Konzept wird bereitgestellt, um Bindungs zu definierten – erstmals kann ein Two-Binding mit einem einzigen Funktionsaufruf definiert werden. model() liefert ein schreibbares Signal inkl. set() und update() Methoden. Obwohl weiterhin intern ein EventEmitter verwendet wird, um die Parent Component zu benachrichtigten – dieses Konzept bleibt gleich – wird keine emit()-Methode angeboten. Stattdessen wird intern automatisch emit() aufgerufen, sobald per set() oder update() ein neuer Wert gesetzt wird.
readonly selected = model(false);
Um auch für den bisherigen @Output() ein dekoratorloses API anzubieten, gibt es nun die Funktion output<string>(). Diese retourniert einen OutputEmitterRef inkl. emit()-Methode – wie bisher.
protected readonly firstnameChange = output<string>();
Die Frage ist nun – warum verwendet man nicht immer model() anstatt einer Kombination von input() und output()? Möchte man wirklich jede Komponenten-interne Änderung sofort an die Elternkomponente weitergeben, dann ist die Verwendung von model() ideal und einfacher als bisher. Wäre das Model Signal aber z. B. mit einem Input Control in UI Component verbunden, will man wahrscheinlich nicht jedes neu eingegebene Zeichen an die Parent Component senden. Diese könnte gegebenenfalls bei jedem Event-Trigger einen http-Call senden. Stattdessen macht es mehr Sinn, die Elternkomponente erst dann zu informieren, wenn der Suchknopf geklickt wird. In diesem Fall kann ein manueller emit()-Aufruf über eine output()-Property eine einfach zu realisierende Performance-Optimierung sein.
Signals und RxJS
Signals bieten zwar, wie RxJS, ein reaktives Konzept an, aber trotzdem gibt es einige relevante Unterschiede. Signals können RxJS auch nicht in allen Bereichen ersetzen – trotzdem strebt das Angular Team an, RxJS optional zu machen. Da sich das widersprüchlich anhört, sollten wir das genauer betrachten.
Signal stellen Zustand dar, der sich über die Zeit verändern kann. Der Zustand ist von Beginn an typkonform vorhanden. Ein RxJS Observable bildet einen Strom von Events ab. Hierbei ist nicht garantiert, dass der Stream ein initiales, synchrones Event aussendet oder überhaupt jemals ein einziges Event. Das BehaviorSubject bietet verpflichtend ein initiales, synchrones Event an und verhält sich dies bezüglich einem Signal sehr ähnlich. Auch ein startWith()-Operator kann für ein initiales, synchrones Event sorgen. In beiden Fällen können nachfolgende Operatoren, wie z. B. debounceTime() die synchrone Antwort allerdings wieder zunichtemachen. Zusammengefasst, kann RxJS sehr viel, aber es erfordert die „richtige“ Implementierung, wenn es sich wie ein Signal verhalten soll. Signals haben hingegen ein sehr striktes, klar ausgewähltes Verhalten, von dem man kaum abweichen kann. Das macht sie zwar nicht zum Generalisten, dafür glänzen sie in jener Nische, für die sie vorgesehen sind: am Übergang von unserem JavaScript-Zustand zum Rendering als Browser-DOM und für die generelle Verwaltung von Zustand auch abseits von Komponenten. Sie sind allerdings klar nicht für Event-basierte Kommunikation geeignet – mehr dazu später.
Um RxJS optional machen zu können, sind noch einige Änderungen an öffentlichen APIs notwendig, die aktuell noch auf Observables & Co basieren. Dennoch ist es bereits ersichtlich, wie dieses Ziel erreicht werden soll. Der Kern des Frameworks wird in Zukunft sowohl intern als auch hinsichtlich der öffentlichen APIs keine Abhängigkeit auf RxJS mehr haben. Trotzdem werden auch künftige Angular Versionen Kompatibilität mit RxJS gewährleisten, allerdings könnte ein ng new irgendwann die Frage stellen, ob RxJS verwendet werden soll. Vielleicht zu Beginn mit Standard-Antwort „Yes“ und später „No“. Jene Projekte, die weiterhin RxJS verwenden wollen, können dann von weiterhin gut unterstützten Secondary-Entrypoints bestimmter Framework-Pakete importieren und nur dann wird es notwendig sein, RxJS zu installieren. Im Core-Paket gibt es bereits den Secondary-Entrypoint @angular/core/rxjs-interop. Dieser bietet u. a. die Funktionen toObservable() und toSignal() an, die zwischen den beiden reaktiven APIs übersetzen können.
toObservable() wird eine Signal Referenz übergeben – das Signal wird diesmal also nicht per Funktionsaufruf gelesen. Stattdessen wird intern ein Effect verwendet, der bei Änderung des Signal-Werts ein neues Event im Observable-Stream aussendet. Da ein Effect standardmäßig an den Lebenszyklus der Component gebunden ist, erfolgt in diesem Fall ein automatischer Cleanup – der Effect wird beendet und nicht mehr für weitere Neuausführungen eingeplant. Manuelle Cleanup-Logik kann ebenso definiert werden, falls eine Registrierung unabhängig der Komponente-Lifetime verwaltet werden soll oder der Effect z. B. in einem Root-Level-Service definiert wurde. Hierbei gilt es zu beachten, dass sich alle Eigenheiten des Signals, die in weiterer Folge näher beschrieben werden, auch auf das Observable auswirken:
- Mehrere synchron geänderte Werte werden zusammengefasst
- Der Initialwert kommt aufgrund des Effects erst mit einer asynchronen Verzögerung im Observable-Stream an
- Sendet das Signal keinen Update-Trigger aus, weil der Wert z. B. nicht korrekt immutable aktualisiert wurde, wird auch kein Observable-Event erzeugt
Der umgekehrte Anwendungsfall – also ein Observable in ein Signal umzuwandeln – kann mit toSignal() realisiert werden. Dadurch erfolgt eine sofortige interne Subscription auf den Stream und wird im Falle einer Component bis zum Destroy-Zeitpunkt aufrechterhalten. Die Entwickler:in muss also nicht manuell unsubscriben, sofern das Standardverhalten der automatischen Beendigung beim Zerstören der Komponente das Beabsichtigte ist.
Alle an das Observable gekettete Operatoren wirken sich auch auf das Setzen der Event-Werte im resultierenden Signal aus. Somit kann die volle Funktionsvielfalt von RxJS auch für Signals verwendet werden. Da der Signalwert, wie immer, sehr einfach per Funktionsaufruf gelesen werden kann, hat die Entwickler:in das Beste aus den beiden reaktiven Welten zur Hand: sehr komfortables Registrieren, um über Wertänderungen informiert zu werden (aus dem Signals API) und die Möglichkeit, maßgeschneidertes reaktives Verhalten zu definieren (über die Vielfalt der Operatoren von RxJS). Es ist klar zu empfehlen, in Komponenten verstärkt Signals zu verwenden. RxJS darf zwar weiterhin auch in Components Anwendung finden, ist aber zumeist sinnvoller in den Service-basierten Logik-Schichten hinter den Komponenten – also z. B. den Side-Effects von State Management Implementierungen – aufgehoben.
Ein Fallbeispiel mit Router Component Input Binding, Signal-based Input, RxJS Interop und Reactive Forms findet die Leser:in im Artikel-begleitenden GitHub-Repository. [10]
Listing 1
// passenger-edit.component.ts private passengerService = inject(PassengerService); id = input(0, { transform: numberAttribute }); private passenger = toSignal( toObservable(this.id).pipe( switchMap(id => this.passengerService.findById(id)) ), { initialValue: initialPassenger } );
Signals werden erwachsen
Das Angular Team hat von Beginn an klargestellt, dass es nicht das Ziel verfolgt, Utility-Funktionen, ähnlich der RxJS-Operatoren, für Signals zu veröffentlichen. Stattdessen ist die Zielsetzung, die grundlegenden reaktiven APIs als direkten und gut integrierten Bestandteil des Frameworks bereitzustellen und damit für die überwiegenden Anforderungen der Angular Community ein Tool bereitzustellen, das in den meisten Fällen zwar eine klare Meinung verfolgt, aber nicht bis ins letzte Detail (wie durch RxJS-Operatoren) angepasst werden kann. Als Konsequenz soll jedoch diese API-Familie einfacher zu erlernen sein als es RxJS bisher war. Will man solche auf Signals aufbauende Hilfsfunktionen nutzen, kann man diese entweder selbst entwickeln und über eine generische Library in den eigenen Repositories verwalten oder eine Community Bibliothek verwenden. Ein Beispiel dafür sind die ngxtension [11]. Wie immer gilt, sorgfältig zu entscheiden für welche Zwecke der Mehraufwand für die Verwaltung von Drittanbieterpaketen gerechtfertigt ist und wann man lieber – wenn das die jeweilige Lizenz erlaubt – sinnvolle, schlanke Implementierungen in die eigene Codebase übernehmen möchte. Ein Beispiel für eine hilfreiche, simple Utility-Funktion ist direkt mit dem vorhergehenden Kapitel verbunden: hat man ein Signal aus Ausgangspunkt, dessen Verhalten man mithilfe mehrerer RxJS Operatoren maßgeschneidert anpassen möchte, um dann am Ende das Resultat wieder als Signal im Template zu lesen, dann ist eine verschachtelte Verwendung von toSignal() und toObservable() sinnvoll. Dafür kann man alternativ auch eine generische Funktion schreiben [12].
Listing 2
// passenger-edit.component.ts private passengerService = inject(PassengerService); id = input(0, { transform: numberAttribute }); private passenger = signalOperators(this.id, pipe( switchMap(id => this.passengerService.findById(id)) ), initialPassenger);
Das Angular Team trifft Design-Entscheidungen oft auch aus pragmatischen Gründen. Obwohl man, wie es als Nicht-Ziel sieht, für jeden denkbaren Anwendungsfall eine Signal-basierte Hilfsfunktion oder ein neues Signal API im Framework direkt anzubieten, weicht man von dieser Strategie gelegentlich ab, wenn damit ein klarer Mehrwert geschaffen werden kann. Ein Beispiel dafür ist das linkedSignal(). Zunächst wurde in einem Video-Interview [13] das Reset-Pattern vorgestellt und darauf verwiesen, dass man sich bei Bedarf vergleichbare Funktionalität auf Basis der bestehenden APIs selbst implementieren kann. Mit Version 19 wurde diese Funktionalität schlussendlich doch als Framework API veröffentlicht. Die Idee dahinter ist, dass man Signale miteinander in Verbindung setzen kann, ohne einen Effect verwenden zu müssen. Zusammengefasst, ist das linkedSignal() eine Kombination aus dem Writable- und dem Computed-Signal. Bei der Definition werden ein Source-Signal und eine Computation-Funktion angeführt. Gleichzeitig erlaubt das Linked Signal aber auch die Verwendung der Schreib-APIs set() und update(). Der hiermit gesetzt Zustand ist so lange gültig, bis das Source-Signal einen neuen Wert aussendet, damit die Computation-Funktion ausführt und so den aktuellen Signal-Wert überschreibt – deshalb der Name Reset-Pattern.
Wofür kann man so ein API in der Praxis verwenden?
Es hilft z. B. beim Refactoring von @Input() zu input(). Wie besprochen, ist der Signal-basierte Input readonly. Daher könnte man das Input Signal als Source für ein Linked Signal definieren und somit einen Komponenten-internen Zustandscontainer schaffen, dessen aktueller Wert nur per Bedarf per Output-EventEmitter zur Elternkomponente gesendet wird [14].
Ein weiter Anwendungsfall kann der Einsatz in einer Auswahlbox sein. Wird im ersten Select-Control ein Land ausgewählt, ändern sich die Stammdaten der Regionen im zweiten Feld. Bei Auswahl „Deutschland“ werden die Regionen „Bayern, Hessen, Hamburg, etc.“ verknüpft, bei „Österreich“ hingegen „Wien, Niederösterreich, Steiermark, etc.“. Bildet man die Auswahl mit linkedSignal() ab, hat der User die Möglichkeit eine konkrete Region auszuwählen, die als lokaler State im Signal-Container verwaltet wird. Ändert sich später das Land, werden die Regionen per neuem Trigger durch das verknüpfte Source-Signal aktualisiert und die Computation wird erneute angestoßen, wodurch initial z. B. die alphabethisch erstgereihte Region vorausgewählt wird. Ab nun kann durch User-Interaktion erneut eine passende Region ausgewählt werden, die ins Linked Signal geschrieben wird und so lange gültig bleibt, bis die Computation erneute angetriggert wird – eben das ist das Reset-Verhalten [15].
„Bei Wertänderung wird die asynchrone Loader-Funktion angetriggert und bisherige Abläufe werden abgebrochen.“
Asynchronität auch ohne RxJS
Noch mehr Aufsehen hat allerdings eine andere Angular 19 Neuerung ausgelöst – die Familie der Resource APIs. Damit ist es nun erstmals möglich ohne direkte Effect-Verwendung und ohne RxJS Interop APIs Signals mit asynchronen Abläufen in Verbindung zu setzen. Die hier eingesetzten Konzepte lösen ein Problem, das durch das aktiv angestrebte Signal-Verhalten geschaffen wurde: Signals, die immer einen synchron verfügbaren Zustand haben müssen, vertragen sich nicht so gut mit der Natur von asynchronen Operationen. Daher hat man nicht einfach wie bei RxJS Hilfsfunktionen geschaffen, die Signals in Promises transformieren können – diese beiden APIs haben zu wenige Gemeinsamkeiten – sondern eine komplexere Signatur entworfen, um die synchrone mit der asynchronen Welt zu verbinden. Am Ende entsteht nicht nur einzelnes Signal, sondern gleich ein Objekt, das mehrere Signals als Properties anbietet.
Alle Resource APIs sind aktuell experimentell und es ist erkennbar, dass das API zwar schon gut durchdacht, aber die Gesamtstory noch nicht fertiggestellt ist. Seit Angular 19 gibt es die Funktionen resource() und rxResource() [16]. Version 19.2 hat uns die httpResource() spendiert. Damit kann Asynchronität über Promises, über Observables oder mithilfe einer internen Abhängigkeit auf den HttpClient angetriggert werden.
resource() kommt daher ohne RxJS Dependency aus und kann direkt aus @angular/core importiert werden. Im Vergleich zu einer direkten Promise-Verwendung in einem Effect, werden hiermit einige Nachteile behoben: die Resource verhindert Race-Conditions, indem ein Signal, das die asynchrone Loader-Funktion triggert auch zum Abbruch eines ggf. noch laufenden vorhergehenden Aufrufs führt. Dies macht ein Abort-Signal als Eingehender-Parameter in die Lambda-Funktion möglich. Dabei gilt es zu beachten, dass das Abort-Signal ein (leider nicht sehr ergonomisches) Sprachkonstrukt von EcmaScript ist, um asynchrone Operationen abbrechen zu können – sie haben allerdings abseits der Namensgleichheit nichts mit dem Angular Signals API zu tun. Zusammengefasst, haben wir also ein Signal, das bei Wertänderung die Promise-basierte asynchrone Loader-Funktion antriggert und bisherige, noch andauernde Abläufe abbrechen kann (vgl. switchMap() Verhalten in RxJS). Auch ein Reload wird unterstützt, wenn man z. B. Daten vom Backend API erneut abrufen will, obwohl sich z. B. die ID als Signal-Trigger nicht verändert hat. In diesem Fall werden erneute Reload-Trigger so lange ignoriert bis der Neuladeablauf beendet ist (vgl. exhaustMap() Verhalten in RxJS). Diese beiden Verhaltensweisen passen vermutlich für die allermeisten Anwendungsfälle sehr gut. Will man allerdings ein abweichendes Verhalten definieren, das bei neuen Triggern angewendet wird, gibt es aktuell keine Möglichkeit dies zu konfigurieren und dabei wird es voraussichtlich auch bleiben. Hier bestätigt sich also der zuvor beschriebene Angular-Weg: klare Meinung, einfache Anwendbarkeit, aber nicht so viele Detailauswahlmöglichkeiten, als wir von RxJS gewohnt sind.
Listing 3
// passenger.service.ts findByIdAsResource(id: Signal<number>): ResourceRef<Passenger | undefined> { return resource({ params: id, loader: ({ params: id, abortSignal }) => fetch( [this.baseUrl, 'passenger', id].join('/'), { signal: abortSignal } ).then(res => res.json() as Promise<Passenger>) }); }
Braucht man also mehr Design-Freiheit, bietet es sich erneut an, RxJS miteinzubeziehen – dafür gibt es die rxResource() aus @angular/core/rxjs-interop. Hiermit kann der Loader anstelle eines Promises per Observable-Stream inkl. Folgeevents implementiert werden. Erneut triggert das Signal den Loader-Stream und verwendet hierfür das switchMap-Verhalten.
Listing 4
// passenger.service.ts findAsResource( filter: Signal<{ firstname: string, lastname: string }> ): ResourceRef<Passenger[] | undefined> { return rxResource({ params: filter, stream: ({ params: filter }) => this.find(filter.firstname, filter.lastname) }); }
Last but not least, bietet die httpResource() aus @angular/common/http ein einfach zu verwendendes Interface an, um lesende http-Anfragen verwalten zu können. Wenngleich es derzeit noch keine Möglichkeit gibt, Daten über einen Backend-Call per PUT oder HOST verändern zu können (Projektname Mutation API), zeigt die httpResource() schon jetzt, wie eine Zukunft ohne RxJS, aber mit ergonomischen Signal APIs aussehen könnte. Im aktuellen experimentellen Status ist es zwar nicht zu empfehlen, die gesamte Codebase von der aktuellen HttpClient Verwendung auf die httpResource() umzustellen, aber eine künftige http-Implementierung könnte ohne interne RxJS-Abhängigkeit auskommen und die öffentliche Signatur der httpResource() müsste nicht zwingend verändert werden [17].
Listing 5
// passenger-edit.component.ts id = input(0, { transform: numberAttribute }); protected passengerResource = httpResource<Passenger>(() => ({ url: 'https://demo.angulararchitects.io/api/passenger', params: { id: this.id() } }), { defaultValue: initialPassenger });
Bleib informiert
Brancheninsights im Newsletter erhalten:
Fazit
Die Signal Story ist mittlerweile schon sehr weit fortgeschritten. Das bedeutet für bestehende Codebases, das spätestens jetzt, wo die meisten APIs bereit stabil verfügbar sind, verstärkt auf Signals gesetzt werden sollte. Dies macht den eigenen, reaktiven Code leichtgewichtiger und einfacher zu lesen als dies zumeist bei RxJS der Fall war. Dennoch wird uns RxJS jedenfalls noch mittelfristig begleiten bis auch asynchrone Abarbeitungen, wie Backend-API-Kommunikation stabil per Resource samt Mutation API unterstützt werden. Bei Einsatz von maßgeschneiderter, reaktiver, service-basierter Logik, wie sie auch in Side-Effects von Enterpise State Management Patterns (Redux und Flux mit @ngrx/store oder @ngrx/signals) vorkommt, ist ebenfalls noch offen, ob es eine „bessere“ Lösung als RxJS geben wird, denn für event-basierte Datenströme sind Signals nicht geeignet. Es spricht also nichts dagegen, bestehenden RxJS-Code weiterzuverwenden bzw. auch auf Observables und Operatoren zurückzugreifen, wenn spezifisches Verhalten modelliert werden soll, das mit Signals nicht gut abbildbar ist.
Links & Literatur
[1] https://angular.dev/reference/migrations/signal-inputs
[2] https://angular.dev/reference/migrations/outputs
[3] https://angular.dev/reference/migrations/signal-queries
[4] https://angular.dev/reference/migrations/control-flow
[5] https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob
[6] https://github.com/tc39/proposal-signals
[7] https://github.com/proposal-signals/signal-polyfill/blob/main/src/signal.ts
[8] https://ngrx.io/guide/signals/rxjs-integration#rxmethod
[9] https://angular.dev/api/router/withComponentInputBinding
[10] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/checkin/src/lib/feature-passenger/passenger-edit/passenger-edit.component.ts
[11] https://ngxtension.netlify.app
[12] https://github.com/mikezks/windev-signal-apis/blob/main/libs/shared/core/src/lib/util-signals/signal-operators.ts
[13] https://youtu.be/aKxcIQMWSNU?t=520
[14] https://github.com/mikezks/windev-signal-apis/blob/linked/libs/domain/booking/src/lib/ui-flight/flight-card/flight-card.component.ts
[15] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/booking/src/lib/feature-flight/airport/airport.ts
[16] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/checkin/src/lib/logic-passenger/data-access/passenger.service.ts
[17] https://github.com/mikezks/windev-signal-apis/blob/http-resource/libs/domain/checkin/src/lib/feature-passenger/passenger-edit/passenger-edit.component.ts
🔍 Frequently Asked Questions (FAQ)
1. Was sind Angular Signals?
Angular Signals sind neue reaktive Primitive im Angular-Framework, die einen synchronen, sofort verfügbaren Zustand bereitstellen. Sie ermöglichen eine direkte, performante Synchronisation zwischen JavaScript-Zustand und DOM – ohne async/await, without subscribe/unsubscribe und ohne AsyncPipe.
2. Was können Angular Signals besonders gut?
Signals glänzen überall dort, wo abgeleiteter Zustand und UI-Synchronisation gefragt sind. Sie liefern Werte synchron, vermeiden unnötige Re-Renders, fassen schnelle Update-Serien zusammen und helfen, die Framework-Komplexität zu reduzieren. Dadurch reagieren Komponenten effizienter und vorhersehbarer.
3. Wofür werden Angular Signals in der Praxis genutzt?
Signals kommen vor allem zum Einsatz bei:
- State Management innerhalb von Komponenten
- reaktiven Bindings wie input(), model() oder output()
- abgeleiteten Werten mit computed()
- Side-Effects über effect()
- sowie asynchronen Operationen über Resource APIs (resource(), rxResource(), httpResource()).
4. Wer sollte Angular Signals einsetzen?
Jedes Angular-Team, das neue Features entwickelt oder bestehende Codebases modernisieren möchte, sollte Signals nutzen. Mit Angular 17–21 sind die APIs stabil geworden, und das Angular-Team empfiehlt ausdrücklich, Signals für neue Features standardmäßig einzusetzen.
5. Warum setzt Angular auf Signals und nicht weiter ausschließlich auf RxJS?
RxJS bleibt weiterhin wichtig, ist aber für viele grundlegende UI-Synchronisationsaufgaben überdimensioniert. Signals bieten einen einfacheren, deterministischen und eingebauten Weg, Zustand und DOM miteinander zu verbinden. Für komplexe Event-Streams oder spezielle Reaktivität bleibt RxJS weiterhin das Mittel der Wahl.
6. Wie verändert Signals das Entwickeln von Angular-Components?
Mit Signals können Inputs (input()), Two-Way-Bindings (model()) und Outputs (output()) ohne Decorators definiert werden. Der Zustand ist ohne AsyncPipe lesbar, und Change Detection wird schlanker. Dadurch werden Komponenten klarer, leichter zu lesen und simpler zu refactoren.
7. Was bedeutet die Einführung der Resource APIs für asynchrone Prozesse?
Die Resource APIs ermöglichen erstmals eine reaktive Asynchronität ohne RxJS. Sie verhindern Race Conditions, unterstützen Reload-Mechanismen und liefern mehrere Signals (z. B. value, loading, error) in einem konsistenten Objekt. Das macht datengetriebene Komponenten einfacher und robuster, auch wenn die API derzeit noch experimentell ist.




