Angular Signals: Die Zukunft des State Management in Angular

Angular Signals ist ein neues Feature in Angular 16, das die Art und Weise, wie Änderungen in Angular-Anwendungen erkannt werden, revolutionieren soll. Seid Angular 17, sind Signals nicht mehr in der DevPreview, sondern fester Bestandteil des Frameworks. Signals bieten eine granularere und effizientere Möglichkeit, Zustandsänderungen zu verfolgen, was zu erheblichen Leistungsverbesserungen führen kann, insbesondere in großen und komplexen Anwendungen.

Was sind Signals?

Ein Signal ist ein Wrapper um einen Wert, der interessierte Benutzer benachrichtigen kann, wenn sich dieser Wert ändert. Signals können beliebige Werte enthalten, von einfachen Primitiven bis hin zu komplexen Datenstrukturen. Signals können schreibbar oder schreibgeschützt sein.

Signals sind Observables (RxJS) ähnlich, aber es gibt einige wichtige Unterschiede. Erstens sind Signals dazu gedacht, Änderungen zu erkennen, während Observables vielseitiger sind. Zweitens sind Signals unveränderlich, d.h. sie können nicht direkt geändert werden. Stattdessen muss ein neues Signal mit dem aktualisierten Wert erzeugt werden. Dadurch werden Signals leichter verständlich und unerwartete Nebeneffekte werden vermieden.

Hier ist ein Beispiel für die Verwendung von Signals in einer Angular-Anwendung:

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

export class AppComponent {
  count: WritableSignal<number> = signal(0);
  doubleCount: Signal<number> = computed(() => this.count() * 2);

  increment() {
    this.count.set(this.count() + 1);
  }

  decrement() {
    this.count.set(this.count() - 1);
  }
}

In diesem Beispiel werden zwei Signals definiert: count und doubleCount. Das Signal count ist ein schreibbares Signal, d.h. sein Wert kann geändert werden. Das Signal doubleCount ist ein berechnetes Signal, d. h. sein Wert wird aus dem Signal count abgeleitet.

Die computed Funktion erhält als Argument eine Ableitungsfunktion. Die Ableitungsfunktion wird immer dann aufgerufen, wenn sich die Signals, von denen sie abhängt, ändern. In diesem Fall hängt das Signal doubleCount vom Signal count ab. Das bedeutet, dass das Signal doubleCount immer dann aktualisiert wird, wenn sich das Signal count ändert.

Um die Signals zu verwenden, können wir sie einfach abonnieren. Wir könnten z.B. das Signal doubleCount abonnieren und das DOM aktualisieren, wenn sich sein Wert ändert:

<p>The double count is {{ doubleCount | async }}</p>

Warum sollte man Signals verwenden?

Die Verwendung von Signals in Angular-Anwendungen hat mehrere Vorteile:

  • Performance: Signals können zu erheblichen Performance-Verbesserungen führen, indem sie die Anzahl der notwendigen Änderungserkennungen reduzieren. Dies liegt daran, dass Signals granularer sind als der aktuelle Mechanismus zur Erkennung von Änderungen, der auf Dirty Checking basiert.
  • Vereinfachung: Signals sind einfacher zu verwenden und zu verstehen als der derzeitige Mechanismus zur Erkennung von Änderungen. Dies liegt daran, dass Signals unveränderlich sind und eine klare API haben.
  • Flexibilität: Signals können zur Implementierung einer Vielzahl unterschiedlicher Reaktionsmuster verwendet werden, z.B. abgeleitete Signals, Memorisierung und Lazy Loading.

Einzigartige Use Cases für Signals

Hier sind einige einzigartige Anwendungsfälle für Signals in Angular-Anwendungen:

  • Synchronisierung von Echtzeitdaten: Signals können verwendet werden, um Echtzeitdaten zwischen verschiedenen Komponenten in einer Angular-Anwendung zu synchronisieren. Dies kann bei der Erstellung von Anwendungen wie Chat-Apps und Dashboards nützlich sein.
  • Effiziente Animation: Signals können verwendet werden, um Elemente in einer Angular-Anwendung effizient zu animieren. Dies liegt daran, dass Signals verwendet werden können, um Zustandsänderungen zu verfolgen und das DOM nur bei Bedarf zu aktualisieren.
  • Lazy Loading: Signals können verwendet werden, um „Lazy Loading“ von Komponenten und Modulen in einer Angular-Anwendung zu implementieren. Dies kann die Performance von Anwendungen verbessern, indem nur die Komponenten und Module geladen werden, die tatsächlich benötigt werden.

Weiterführende Use Cases für Signals

Zusätzlich zu den oben genannten Anwendungsfällen können Signals auch verwendet werden, um fortgeschrittenere Reaktionsmuster zu implementieren, wie z.B:

  • Zustandsautomaten: Signals können verwendet werden, um Zustandsautomaten in Angular-Anwendungen zu implementieren. Dies kann nützlich sein, um komplexe Anwendungen mit mehreren Zuständen zu erstellen.
  • UI Interaktionen: Signals können verwendet werden, um komplexe UI Interaktionen zu implementieren, z.B. Drag and Drop und Resize.
  • Datenvalidierung: Signals können verwendet werden, um Datenvalidierung in Angular-Anwendungen zu implementieren. Dies kann nützlich sein, um sicherzustellen, dass die vom Benutzer eingegebenen Daten gültig sind.

Beispiele für den Einsatz von Signals

Hier sind einige Beispiele für die Verwendung von Signals in Angular-Anwendungen:

Beispiel 1: Synchronisation von Echtzeitdaten

Das folgende Beispiel zeigt, wie Signals verwendet werden, um Echtzeitdaten zwischen zwei Komponenten zu synchronisieren:

import { signal } from '@angular/core';

export class ChatComponent {
  messages: Signal<string[]> = signal([]);

  sendMessage(message: string) {
    this.messages.push(message);
  }
}

export class MessageListComponent {
  messages: Signal<string[]> = signal([]);

  ngOnInit() {
    this.messages.subscribe(messages => {
      this.messages = messages;
    });
  }
}

In diesem Beispiel hat die ChatComponent ein Signal namens messages, das ein Array mit Nachrichten enthält. Die MessageListComponent hat ebenfalls ein Signal namens messages, das ein Array von Nachrichten enthält.

Wenn der Benutzer eine Nachricht an die ChatComponent sendet, wird das Signal messages aktualisiert. Die MessageListComponent ist auf das Signal messages subscribed, so dass sie aktualisiert wird, wenn sich das Signal messages ändert. Dadurch wird sichergestellt, dass die MessageListComponent immer die neuesten Nachrichten anzeigt.

Beispiel 2: Effiziente Animation

import { signal } from '@angular/core';

export class AppComponent {
  // Define a signal for the element to be animated.
  element: Signal<HTMLElement> = signal(null);

  // Subscribe to the signal and update the DOM accordingly.
  ngOnInit() {
    this.element.subscribe(element => {
      // Animate the element.
    });
  }

  // Update the state using a writable signal.
  setElement(element: HTMLElement) {
    this.element.set(element);
  }
}

In diesem Beispiel verwenden wir ein Signal, um das zu animierende Element zu verfolgen. Außerdem abonnieren wir das Signal und aktualisieren das DOM entsprechend, wenn das Signal ausgelöst wird. Schließlich verwenden wir ein beschreibbares Signal, um das zu animierende Element zu aktualisieren.

Um dieses Beispiel zu verwenden, müssen wir zuerst ein Template erstellen, das das zu animierende Element enthält. Zum Beispiel:

<div id="my-element"></div>

Dann müssen wir die AppComponent in Ihre Komponente injizieren und das Element dem element Signal zuweisen. Zum Beispiel:

import { Component } from '@angular/core';
import { AppComponent } from './app.component';

@Component({
  selector: 'my-component',
  templateUrl: './my-component.component.html'
})
export class MyComponent {
  constructor(private appComponent: AppComponent) {}

  ngOnInit() {
    this.appComponent.element.set(document.getElementById('my-element'));
  }
}

Zuletzt müssen wir den Code schreiben, um das Element zu animieren. Zum Beispiel:

import { animate, style } from '@angular/animations';

@Component({
  selector: 'my-component',
  templateUrl: './my-component.component.html',
  animations: [
    animate('1s', style({
      transform: 'translateY(100px)'
    }))
  ]
})
export class MyComponent {
  constructor(private appComponent: AppComponent) {}

  ngOnInit() {
    this.appComponent.element.set(document.getElementById('my-element'));
  }

  animate() {
    // Animate the element.
    this.appComponent.element.value.classList.add('animated');
  }
}

Wenn wir die Methode animate() aufrufen, wird das Element animiert, so dass es sich auf der Seite um 100 Pixel nach unten bewegt.

Beispiel 3: Derived Signals

Das folgende Beispiel zeigt, wie man Signals verwendet, um ein abgeleitetes Signal zu implementieren:

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

export class AppComponent {
  count: WritableSignal<number> = signal(0);
  isEven: Signal<boolean> = computed(() => this.count() % 2 === 0);

  increment() {
    this.count.set(this.count() + 1);
  }

  decrement() {
    this.count.set(this.count() - 1);
  }
}

In diesem Beispiel ist das isEven Signal ein abgeleitetes Signal, das vom count Signal abhängt. Jedes Mal, wenn sich das count Signal ändert, wird das isEven Signal entsprechend aktualisiert.

Das isEven Signal kann dann zur bedingten Darstellung von Elementen im DOM verwendet werden. Beispielsweise könnte eine andere Farbe gerendert werden, je nachdem, ob das isEven Signal true oder false ist:

<p class="even" *ngIf="isEven | async">The count is even.</p>
<p class="odd" *ngIf="!isEven | async">The count is odd.</p>

Beispiel 4: Memoization

Das folgende Beispiel zeigt, wie Signals zur Implementierung der Memoisierung verwendet werden können:

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

export class AppComponent {
  expensiveComputation: Signal<number> = memoized(() => {
    // Perform an expensive computation here.
    return 123;
  });

  render() {
    // Display the result of the expensive computation.
    return this.expensiveComputation();
  }
}

In diesem Beispiel ist das Signal expensiveComputation ein memoized Signal. Das bedeutet, dass die Berechnung nur einmal durchgeführt und das Ergebnis zwischengespeichert wird. Spätere Aufrufe des Signals expensiveComputation geben einfach das zwischengespeicherte Ergebnis zurück.

Dies kann nützlich sein, um die Leistung von Anwendungen zu verbessern, die teure Berechnungen durchführen.

Beispiel 5: Lazy Loading

Das folgende Beispiel zeigt, wie Signals verwendet werden können, um Lazy Loading zu implementieren:

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

export class AppComponent {
  modules: Signal<Array<() => Promise<any>>> = signal([]);

  loadModule(moduleName: string) {
    const moduleLoader = lazy(() => import(`./modules/${moduleName}.module`));
    this.modules.push(moduleLoader);
  }
}

In diesem Beispiel enthält das Signal modules ein Array mit Funktionen zum Laden von Modulen. Wenn der Benutzer auf eine Schaltfläche zum Laden eines Moduls klickt, wird die Methode loadModule() aufgerufen. Diese Methode fügt dem Signal modules eine Modulladefunktion hinzu.

Das Signal modules wird dann subskribiert. Jedes Mal, wenn sich das Signal modules ändert, werden die Funktionen des Modulladers ausgeführt. Dadurch werden die Module bei Bedarf geladen.

Fazit

Angular Signals ist ein mächtiges neues Feature, das verwendet werden kann, um die Performance, Einfachheit und Flexibilität von Angular-Anwendungen zu verbessern. Obwohl sich Signals noch in der Developer Preview befindet, lohnt es sich, einen Blick darauf zu werfen, wenn Sie nach Möglichkeiten suchen, Ihre Angular-Anwendungen zu verbessern.