Angular 16 – ein längst überfälliges Lebenszeichen?!

Das Angular-Team hat ein großes Update veröffentlicht und damit eine ganze Bandbreite an Änderungen, die gegen Unzulänglichkeiten gegenüber anderen neueren Webframeworks wirken sollen. Erfahre mehr dazu in diesem Blogpost.

Das Angular-Team hat im Mai die neueste Version des beliebten Frameworks veröffentlicht: Angular 16. Mit dem Update gibt es eine ganze Bandbreite an Änderungen, die gegen Unzulänglichkeiten gegenüber anderer neuerer Webframeworks wirken sollen. Lasst uns einen genaueren Blick auf diese vermeintlichen Defizite werfen und wie das Angular-Team diese angehen will. In diesem Blogpost werden wir einen tieferen Einblick in die neue Vorgehensweise in Bezug auf Reaktivität nehmen.

Was hat sich seit dem letzten Update von Angular 15 getan?

In der neuesten Version von Angular gibt es eine neue Möglichkeit, vollständig reaktive Werte zu definieren. Reaktivität beschreibt die Fähigkeit eines Webframeworks, auf Updates der Nutzeroberfläche zu reagieren. Das geschieht normalerweise, wenn eine Eingabe getätigt wird oder sich die Daten von außerhalb verändern. Durch diesen Mechanismus wird sichergestellt, dass die Nutzeroberfläche immer die aktuellen Daten hält – und das ohne ein manuelles Neuladen der Seite.

Die Veränderungserkennung (im folgenden Change Detection), die Reaktivität ermöglicht, war bis jetzt abhängig von einer Bibliothek namens Zone.js. Unabhängig davon, dass diese eine große Bundlegröße hat, wird die Change Detection im Standardfall bei jeder Änderung auf dem gesamten Komponentenbaum durchgeführt. Durch die ChangeDetectionStrategy.OnPush können wir diese Performanceproblematik schon teilweise umgehen, aber wäre es nicht großartig, wenn wir das auch erreichen können, ohne zusätzliche Ressourcen auszuliefern. Mit Signals hat Angular die Fähigkeit, Veränderungen zu erkennen und diese der Nutzeroberfläche zu übermitteln – ohne die vorher genannte Bibliothek Zone.js.

Writable bzw. überschreibbare Signale

Signals ist ein neuer primitiver Typ, der Werte wie folgt speichern kann:

const price: WritableSignal<number> = signal(25);

Im Gegensatz zu einem Observable brauchen Signals immer einen Initialwert.

Dieses Signal hält nun eine Zahl (Typ number), welches den Preis eines Artikels enthält. Mithilfe der Funktion this.price() kann auf den Wert zugegriffen werden. Zusätzlich kann mithilfe der Setter-Funktion this.price.set(26) der Preis überschrieben werden.

Über eine Updatefunktion können wir einen neuen Preis abhängig vom vorherigen Wert festlegen:

this.price.update(price => price + 1)

Zu guter Letzt gibt es eine Mutationsfunktion, die es uns ermöglicht, nur einen einzelnen Wert innerhalb eines Objektsignals zu verändern, ohne das gesamte Objekt neu zuzuweisen.

this.ball.mutate(value => {value.price = 43});const ball = signal({name: 'ball', price: 42});
...
const ball = signal({name: 'ball', price: 42});

In diesem Beispiel macht es wenig Unterschied, ob wir nur einen einzelnen Wert ändern oder das gesamte Objekt, aber stell dir dieses vor, wenn wir eine große Liste aus komplexen Objekten haben.

Computed bzw. berechnete Signale

Wir haben die Möglichkeit, ein Signal zu deklarieren, dessen Wert von einem anderen (oder mehreren) abgeleitet wird. Bleiben wir bei dem Beispiel mit den Variablen, die Preise repräsentieren. Angenommen, wir möchten, dass die Summe zweier Werte automatisch berechnet wird, wenn sich einer der beiden Werte ändert, dann können wir unser berechnetes Signal wie folgt definieren

const bellPrice: WritableSignal<number> = signal(21);
const helmetPrice: WritableSignal<number> = signal(45); 
const sumOfPrices: Signal<number> = computed(() => bellPrice() + helmetPrice());

Angular weiß nun, dass es alle Vorkommen von „sumOfPrices“ aktualisieren muss, wenn sich der Preis der Glocke oder des Helms ändert. Diese berechneten Signale sind für uns Entwickler:innen schreibgeschützt, was die folgenden Vorteile mit sich bringt:

  1. Sie sind lazy-loaded. Die Berechnung ihres Wertes geschieht zum ersten Mal, wenn die Variable tatsächlich gelesen wird.
  2. Sie werden zwischengespeichert („gecacht“). Nach Abschluss der Berechnung wird der Wert gespeichert und nur dann erneut berechnet, wenn sich eine der Abhängigkeiten ändert.

Eine weitere Besonderheit von berechneten Signalen ist, dass sie Bedingungen enthalten können. Wenn wir unser Beispiel mit dem Preis noch einen Schritt weiterführen und die Summe des Preises nur dann anzeigen wollen, wenn ein Benutzer das Recht in der Anwendung hat, können wir dieses Recht in einem Signal auswerten. Das Signal gibt einen booleschen Wert zurück und die Summe wird nur ausgewertet, wenn der boolesche Wert „wahr“ ist.

const showSumOfPrices = signal(false)
const sumOfPrices = computed(() => showSumOfPrices() ? bellPrice() + helmetPrice() : `Privilege to display sum of price is missing.`)

Das Schöne daran ist, dass die Preise der Glocke oder des Helms nie gelesen werden, solange showSumOfPrices auf „false“ gesetzt ist, und somit keine unnötigen Aktualisierungen von sumOfPrices ausgelöst werden. Sobald showSumOfPrices jedoch „true“ ist, werden die Preise gelesen und die Summe der Preise wird ausgewertet, solange es „true“ bleibt.

Welchen Mehrwert bietet uns Angular 16?

Zum Schluss noch ein großer Vorteil: Signale sind garantiert glitchfrei. Stell dir ein Szenario mit RxJS vor, in dem wir zwei Variablen haben, die wir direkt nacheinander ändern. Nehmen wir an, wir haben zwei Produkte, die einen Preis haben, den wir aktualisieren wollen. Wenn wir die neuen Preise direkt nacheinander setzen, werden wir in einen Zwischenzustand geraten, in dem ein Produkt bereits den neu zugewiesenen Wert aufweist, während das andere noch den alten Wert hat.

Mit Signals wird dies nicht passieren.

const bookPrice = signal(10)
this.bookPrice.set(11)
this.blenderPrice.set(33)

Signals wird beide Preise gleichzeitig aktualisieren. Dies ist sehr wichtig, denn wir wollen nicht, dass die Änderungserkennung bei jeder Aktualisierung einer Variablen läuft. Ich persönlich bin vor allem von diesem Punkt begeistert!

Operationen mit Effect()

Eine zusätzliche Operation, die im Zusammenhang mit Signalen nützlich ist, sind Effekte. Sie können durch den Aufruf von effect() und der Übergabe einer Lambda-Funktion als Parameter erzeugt werden. Alle Änderungen an Signalen, die an diese Lambda-Funktion übergeben werden, führen dazu, dass die Funktion im Effekt erneut ausgeführt wird. Ein Effekt wird mindestens einmal ausgeführt. Im Allgemeinen sollten Effekte nicht die Lösung für jedes Problem sein, aber es gibt bestimmte Anwendungsfälle wie Ausführungsverhalten, das die Template-Syntax nicht unterstützt. Sie sollten beispielsweise nicht für die Weitergabe von Zustandsänderungen in der Anwendung verwendet werden.

Eines der möglichen Ergebnisse sind unendliche zirkuläre Aktualisierungen oder, weniger kritisch, unnötige Ausführungen der Änderungserkennung. Die häufigste Art, einen Effekt einzurichten, ist das Hinzufügen zum Konstruktor, der Komponente, des Dienstes oder Ähnlichem. Sie können nützlich sein, wenn eine Protokollierung erforderlich ist, oder möglicherweise, wenn man versucht, ein bestimmtes Verhalten schnell zu debuggen/zu verifizieren.

Bei der Zerstörung der entsprechenden Komponente des Dienstes o. ä. wird auch ein Effekt zerstört.

Das ist ja alles schön und gut, aber wie und warum funktioniert das überhaupt?

Zunächst einmal herzlichen Glückwunsch, dass du es bis hierher geschafft hast 😉

Nachdem ich Artikel gelesen, Videos über Signale gesehen und sie schließlich selbst ausprobiert habe, war ich neugierig, wie sie überhaupt funktionieren. Werfen wir einen kurzen Blick darauf!

Wenn wir ein neues Signal price = Signal(39) initialisieren, erhalten wir ein neues Signal vom Typ Zahl(number). Abgesehen davon wird das Signal einer Liste von Beobachtern hinzugefügt. Wenn wir den Preis aktualisieren, indem wir price.set(40) ausführen, wird diese Liste von Beobachtern über die Änderungen informiert. Das bedeutet, dass ohne einen Aufruf von price() nichts passieren wird. Erst der Aufruf von price() fügt die Variable zur Liste der Beobachter hinzu.

Kurz gesagt, der Aufruf von set() für ein Signal benachrichtigt alle Vorkommnisse, bei denen der Getter für dieses spezielle Signal aufgerufen wird. Das Gleiche gilt für effect(). Wann immer ein Wert aktualisiert wird, werden alle Beobachter über die Änderungen benachrichtigt.

Was bedeutet das für uns als Angular-Entwickler:innen?

Da sich Signals in der Version 16 noch in der Entwickler-Preview befindet und du sicher nicht (Teile) der Anwendung zweimal neu schreiben willst, wird dringend empfohlen, keine gesamte Anwendung, die in Produktion ist, auf Signals umzustellen. Du könntest jedoch mit kleinen Blattkomponenten beginnen, von denen wenig bis gar keine andere Komponenten abhängig sind, um einen ersten Blick auf diese großartige neue Funktionalität zu werfen. Abgesehen davon ist es immer ratsam, mit den Abhängigkeiten eines Projekts auf einem relativ neuen Stand zu sein. Um darauf vorbereitet zu sein, könntest du also auf jeden Fall ein Upgrade auf Angular v16 durchführen.

Es wird auch noch ziemlich lange dauern, bis wir aufhören können, uns auf Zone.js zu verlassen und es in unseren Angular-Anwendungen ausliefern zu müssen. Das gilt für Anwendungen, die es schon seit einiger Zeit gibt, neue könnten ganz ohne Zone.js zurechtkommen. Aber irgendwo müssen wir ja anfangen. Meine Empfehlung: Lege dir ein neues Projekt mit Angular 16 an (falls dein aktuelles nicht einfach aktualisiert werden kann) und fang direkt an, selbst zu experimentieren!

Weitere Features von Angular 16

Dieser Blogpost konzentriert sich stark auf das Reaktivitäts-Feature: Signale. Es gibt jedoch noch viele weitere Features und Änderungen, die mit v16 kommen, wie z. B.:

  • SSR
  • Hydratisierung
  • Erforderliche Parameter für Komponenten und eine entsprechende Transformationsfunktion dazu, um nur ein paar zu nennen.

Fragen? Kontaktierte uns gerne über info@novatec-gmbh.de. Gerne sprechen wir mit dir über Fragen und Anregungen.

Allgemeine Anfrage

Wir freuen uns darauf, Ihre Herausforderungen zusammen in Angriff zu nehmen und über passende Lösungsansätze zu sprechen. Kontaktieren Sie uns – und erhalten Sie maßgeschneiderte Lösungen für Ihr Unternehmen. Wir freuen uns auf Ihre Kontaktanfrage!

Jetzt Kontakt aufnehmen