Kolumne: Enterprise Angular

Signals werden erwachsen – das neue Resource API in Angular 19

Signals werden erwachsen – das neue Resource API in Angular 19

Kolumne: Enterprise Angular

Signals werden erwachsen – das neue Resource API in Angular 19


Das in Angular 19 zunächst experimentell eingeführte Resource API ermöglicht das asynchrone Laden von Daten in der Welt von Signals. Das gibt uns endlich die Möglichkeit, HTTP-Zugriffe in den reaktiven Fluss zu integrieren. In diesem Artikel zeige ich, wie man damit ein typisches CRUD-Szenario erstellt. Das verwendete Codebeispiel findet sich unter [1] im Branch 02e-resource.

Beispielanwendung

Zur Demonstration der einzelnen Features nutze ich die bereits aus früheren Artikeln bekannte Demoanwendung Austrian Desserts (Abb. 1).

steyer_kolumne_resource_1

Abb. 1: Beispielanwendung: Austrian Desserts

In dieser Anwendung kann man nach Desserts der Alpenrepublik anhand des originalen österreichischen Namens oder der jeweiligen englischen Übersetzung suchen. Die einzelnen Desserts lassen sich bewerten. Als Alternative können Benutzer:innen auch die Bewertungen eines anerkannten Experten auf diesem Gebiet – es handelt sich dabei um meine Wenigkeit – laden. Außerdem gibt es eine Detailansicht zum Bearbeiten von Desserts (Abb. 2).

steyer_kolumne_resource_2

Abb. 2: Detailansicht in Beispielanwendung

Erste Schritte mit dem Resource API

Jede mit dem Resource API definierte Resource hat eine Funktion loader, die ein Promise mit den geladenen Daten zurückgibt. Dieser Loader kommt unmittelbar nach der Initialisierung der Resource zum Einsatz. Zusätzlich kann die Resource ein request Signal haben, das Parameter (Suchkriterien) für den Loader bereitstellt. Jedes Mal, wenn sich das request Signal ändert, stößt die Resource den Loader erneut an (Listing 1).

Listing 1

import { resource } from '@angular/core';
[…]
@Component([...])
export class DessertsComponent {
  #dessertService = inject(DessertService);
  
  [...]
  
  // Criteria for search
  originalName = signal('');
  englishName = signal('');

  // Combine criteria to computed Signal
  dessertsCriteria = computed(() => ({
    originalName: this.originalName(),
    englishName: this.englishName(),
  }));

  // Define resource with request (=search criteria) and loader
  // Every time, the request is changing, the loader is triggered
  dessertsResource = resource({
    request: this.dessertsCriteria,
    loader: (param) => {
      return this.#dessertService.findPromise(param.request);
    }
  });

  // Initially, resources are undefined
  desserts = computed(() => this.dessertsResource.value() ?? []);

  loading = this.dessertsResource.isLoading;
  error = this.dessertsResource.error;

  // The reactive flow goes on ...
  ratings = signal<DessertIdToRatingMap>({});
  ratedDesserts = computed(() => this.toRated(this.desserts(), this.ratings()));

  [...]
}

In diesem Beispiel verwendet dessertsResource die Werte in den Signals originalName und englishName als Parameter. Das an den Loader übergebene param-Objekt enthält die aktuellen Suchkriterien aus dem request Signal.

Das Ergebnis der Resource findet sich in ihrem Signal value. Das berechnete desserts Signal ersetzt den initialen undefined-Wert durch ein leeres Array. Das Signal isLoaded informiert über den Ladezustand, und error ist ein Signal mit einem möglichen Fehler, der während des Ladens aufgetreten ist.

Bei ratedDesserts handelt es sich um ein weiteres berechnetes Signal. Es repräsentiert die Desserts inkl. der geladenen Bewertungen. Durch die Kombination der Resource mit Signals und computed ergibt sich ein reaktiver Fluss, der sich von der Benutzereingabe über das von Daten bis hin zur deren Projektion auf ein in der Ansicht gebundenes (View)Model erstreckt. Ein Schritt führt reaktiv zum nächsten.

Loader werden nicht überwacht

Es ist wichtig zu beachten, dass Angular das request Signal auf Änderungen überwacht, den Loader jedoch nicht. Das bedeutet, dass eine Änderung am request Signal den Loader anstößt, eine Änderung an einem Signal, das nur im Loader zum Einsatz kommt, jedoch nicht.

Das liegt daran, dass die von Angular implementierte automatische Verfolgung (Auto Tracking) nur im ersten Teil des Loaders, der synchron abläuft, funktionieren würde. Alles, was nach der ersten asynchronen Operation ausgeführt wird, z. B. Code nach await oder in einem then Handler, kann nicht der automatischen Verfolgung unterliegen. Um solche verwirrenden Situationen zu vermeiden, wird der gesamte Loader von der Überwachung ausgenommen.

Race Conditions

In vielen Webanwendungen können Benutzer:innen leicht überlappende Anfragen auslösen. Das ist insbesondere bei einer reaktiven Benutzeroberfläche der Fall, bei der z. B. bereits das Ändern eines Filters einen weiteren Ladevorgang auslöst (Abb. 3).

steyer_kolumne_resource_3

Abb. 3: Überlappende Anfragen

Im skizzierten Fall würden Benutzer:innen erwarten, nur Ergebnisse für „Ice Cream Pancakes“ zu erhalten, obwohl der Filter zuvor auch andere Werte hatte. Es wäre ziemlich verwirrend, wenn kurz Ergebnisse für gewöhnliche „Pancakes“ und „Sacher Cake“ aufblinken würden. Noch verwirrender wäre es, wenn die erste Suche etwas länger dauerte (Abb. 4).

steyer_kolumne_resource_4

Abb. 4: Race Conditions

In diesem Fall würden die unerwünschten Zwischenergebnisse in einer Reihenfolge aufblinken, die nicht mit der Reihenfolge der Anfragen übereinstimmt. Man spricht hier von einer Race Condition.

Das Resource API verfügt über switchMap-Semantik

In Angular kommt typischerweise der RxJS-Operator switchMap zum Einsatz, der solche Situationen verhindert, indem er überlappende Anfragen bis auf die letzte abbricht. Die gute Nachricht ist, dass resource dasselbe Verhalten verwendet. Standardmäßig kann es die vorherige...