Einführung in Angular Signals

Angular hat mit Version 16 als Developer Preview und ab Angular 17 fix integriert eine neue Möglichkeit zur reaktiven Programmierung eingeführt, die Signals.

Ein Signal ist ein Wrapper um einen Wert, der interessierte Verbraucher benachrichtigen kann, wenn sich dieser Wert ändert. Signale können jeden Wert enthalten, von einfachen Grundelementen bis hin zu komplexen Datenstrukturen.

Der Wert eines Signals wird immer über eine Getter-Funktion gelesen, die es Angular ermöglicht, zu verfolgen, wo das Signal verwendet wird.

Signals sind somit eine Möglichkeit, Wert-Änderungen zu den Templates zu übertragen und Angular die Möglichkeit zu geben, nur den Teil des DOM zu aktualisieren, welche diesen Wert auch verwenden. Um dies zu verstehen, müssen wir uns zuerst ansehen, wie Angular bisher wußte, wann die Bereiche der Website zu aktualisieren sind:

Angular Change Detection und Signals

Bisher baute Angular auf einer Library namens zone.js auf. Diese Library machte es grob gesagt möglich, auf jegliche Änderungen im Browser (z.B. ein Button-Klick, eine Formular-Eingabe usw.) reagieren zu können. Sobald eine Änderung ausgelöst wird, geht Angular seine Komponenten im DOM durch und prüft, ob etwas aktualisiert werden muss. Vorteil dieser Methode ist, dass man sich als Programmierer nicht darum kümmern musste. D.h. wenn ich z.B. den Wert einer Variable nach einem Button-Klick ändere und diese im Template angezeigt wird (Binding), ist diese Änderung automatisch sichtbar.

Performance-mäßig ist diese Methode jedoch nie ganz optimal gewesen. Es gibt verschiedene Methoden, um hier Verbesserungen zu bewirken, z.B. indem man seine Komponente mit der OnPush-Strategie kennzeichnet. Danach nimmt die Komponente am Change-Detection Zyklus nur mehr teil, wenn sich seine @Input-Variablen geändert haben.

Mit Signals hat Angular eine neue Methode eingeführt, wie man Angular mitteilen kann, was bei Änderung eines Wertes aktualisiert werden muss.

Was sind Signals?

Ein Signal in Angular besteht immer aus einem Produzenten und einem Konsumenten. Der Produzent erzeugt Daten und der Konsument liest diese Daten. D.h. jedes mal, wenn sich der Wert eines Signals ändert, werden alle Konsumenten darüber informiert und können dementsprechend den neuen Wert verwenden. Signals haben im Prinzip große Ähnlichkeit mit BehaviorSubject’s der rxjs-Bibliothek.

Weiters wird unterschieden zwischen schreibenden Signalen und berechneten Signalen. Ein schreibendes Signal gibt den Wert direkt weiter, während ein berechnetes Signal eine Funktion ist, welche schreibende Signale verwendet und diese modifizieren oder kombinieren kann.

Beispiel für die Verwendung von Angular Signals

Wir wollen uns ein einfaches Beispiel ansehen. Wir haben einen Button, welcher auf Klick eine Zufallszahl erzeugt und sowohl diese als auch den doppelten Wert anzeigt. Um Signals zu verwenden, müssen diese aus der @angular/core-Bibliothek importiert werden. Wir verwenden sowohl eine schreibendes Signal (signal) als auch ein berechnetes (computed):

import {Component, computed, signal} from '@angular/core';

@Component({
  selector: 'app-signal-beispiel',
  standalone: true,
  imports: [],
  template: `
      <div>
          Number: {{number()}}
          <br><br>
          Double: {{double()}}
          <br><br>
      </div>
      <div>
          <button (click)="onClick()">Zufallszahl erzeugen</button>
      </div>
  `,
  styleUrl: './signal-beispiel.component.css'
})
export class SignalBeispielComponent {

  public number = signal(0);

  public double = computed(() => this.number() * 2);

  public onClick() {
    this.number.set(Math.floor(Math.random() * 100))
  }
}Code-Sprache: JavaScript (javascript)
  • Mit import {Component, computed, signal} from '@angular/core'; werden die Signal und Computed-Klassen importiert
  • public number = signal(0); erzeugt ein neues Signal mit dem Initial-Wert 0. Signals haben immer einen definierten Anfangswert.
  • public double = computed(() => this.number() * 2); erzeugt ein berechnetes Signal, welches das number-Signal verwendet und daraus einen neuen Wert berechnet, in diesem Fall einfach verdoppelt.
  • Mit this.number.set(....) wird ein neuer Wert für das Signal gesetzt, und damit auch automatisch eine neue Berechnung des computed-Signal „double“ ausgelöst.
  • Die Ausgabe im Template erfolgt mit Number: {{number()}}. Zu beachten ist die Funktionsklammern nicht vergessen, es handelt sich beim Signal nicht um eine einfache Variable!

Hier finden Sie das Beispiel auf StackBlitz

Die Benutzung von Signals ist also sehr einfach. Der Vorteil für das Framework ist, mit der Verwendung des Signals im Template wird die Komponente automatisch markiert für die Change-Detection, d.h. Angular weiß sofort, wo es sich auswirkt wenn das Signal einen neuen Wert produziert.

Möglichkeiten zum Setzen eines Wertes des Signals

Wir haben schon eine Methode kennengelernt um den Wert eines Signals zu setzen: .set(…). Es gibt aber auch die Möglichkeit, mit .update() einen Wert basierend auf den aktuellen zu setzen:

Nehmen wir an, wir hätten eine Book-Klasse und definieren ein schreibbares Signal als Array von Büchern:

  public books: WritableSignal<Book[]> = signal([]);Code-Sprache: HTML, XML (xml)

Dann können wir ein neues Buch folgendermaßen hinzufügen:

    const book: Book = new Book();
    book.title = 'Neuer Buch-Titel';
    this.books.update(books => [...books, book]);Code-Sprache: JavaScript (javascript)

Typ eines Signals definieren

Wie wir im obigen Beispiel gesehen haben, kann man den Datentyp eines Signals als WritableSignal<typ> deklarieren.

  public books: WritableSignal<Book[]> = signal([]);Code-Sprache: HTML, XML (xml)

Berechnete Signale (computed)

Ein berechnetes Signal setzt sich aus einem oder mehreren Signalen zusammen und berechnet daraus einen neuen Wert. Es wird automatisch erkannt, welche Signale verwendet werden und das computed-Signal nur dann neu berechnet, wenn sich einer oder mehrere der verwendeten Signale im Wert ändern.

Beispiel:

  public wert1 = signal(0);
  public wert2 = signal(0);

  public summe = computed(() => this.wert1() + this.wert2());Code-Sprache: JavaScript (javascript)

Hier würde summe immer nur dann ausgeführt, wenn sich eines der beiden Signale wert1 oder wert2 im Wert ändert.


Effekte

Effekte im Zusammenhang mit Signals sind Programmcode, welcher immer dann ausgeführt wird, wenn sich ein- oder mehrere Signals-Werte ändern. Im Gegensatz zu computed wird hier aber kein Wert zurückgegeben, sondern nur der Programm-Code ausgeführt. Effekte eigenen sich also gut dazu, z.B. Signal-Werte zu loggen.

Beispiel:

  public number = signal(0);

  public logEffect = effect(() => console.log(`Nummer hat sich geändert: ${this.number()}`);Code-Sprache: JavaScript (javascript)

Fazit

Angular bietet mit Signals eine neue und einfache Möglichkeit zur reaktiven Programmierung an. Vor allem für Einsteiger ist das Konzept leichter Verständlich als die vielen Möglichkeiten, die z.B. rxjs mit den Observables bietet. Signals erweitern die Möglichkeiten, große Angular Anwendungen geschwindigkeitsmäßig zu optimieren und die Change-Detection zu beschleunigen.