Angular resources sind aktuell noch als „experimental“ gekennzeichnet, sollen in Zukunft aber die Lücke zwischen Signals und asynchronen Prozessen darstellen. Hierfür gibt es zwei neue Methoden: resource() (promise basiert, einzelner Wert) und rxResource() (Observable basiert, Stream), die uns eine signal basierte Resource erzeugt. Das einfachste aller Beispiele sähe so aus:
todoResource = resource({
loader: () => firstValueFrom(this.http.get('https://dummyjson.com/todos'))
})
<ul>
@for(t of todoResource.value();track t.id){
<li>{{t.todo}}</li>
}
</ul>
Optional können natürlich Parameter oder default-Werte definiert werden. Die so erzeugte ResourceRef bietet nun nicht nur die geladenen Werte (value) als Signal, sondern auch zusätzliche Signals wie error, isLoading oder status.
The Signal Way
Noch spannender wird es nun, wenn Parameter ins Spiel kommen. Diese werden über das Attribut „params“ der Resource zur Verfügung gestellt. Dabei werden alle verwendeten signals getrackt und triggern bei Änderungen (ähnlich wie computed/effect) die erneute Ausführung der Loader-Funktion. Eine Service Methode lässt sich nun wie folgt definieren:
@Injectable({
providedIn: 'root'
})
export class ResourceService {
loadItem(id: () => number | undefined) {
return resource({
params: () => id(),
loader: ({params}) => firstValueFrom(this.http.get<any>(`https://dummyjson.com/todos/${params}`))
})
}
}
Man beachte insbesondere die Methoden-Signatur. Wir erwarten hier also ein Signal, das number oder undefined liefert. Den konkreten Wert lesen wir erst in der params-Funktion. Das volle Potenzial sehen wir z.B. in folgendem Beispiel. Hier kombinieren wir den Signal-basierten Service Aufruf mit dem Input-Signal unserer Komponente = ändert sich das Input Binding der Komponente (jemand selektiert eine andere Id) änder sich das Input-Signal und triggert damit das erneute Ausführen der Resource-Loader Methode mit neuem Parameter.
@Component({
...
template: '{{selectedResource.value() | json}}'
})
export class DemoComponente{
service = inject(ResourceService);
selectedId = input.required<number>()
selectedResource = this.service.loadItem(this.selectedId);
Eager vs. Lazy
Ein wichtiger Punkt hier: die so erstellen Resources sind „eager“, führen ihre Loader-Funktionen also bereits bei der Erzeugung aus (wohingegen ein Observable ja erst bei einem „Subscribe“ aktiv werden würde). Tatsächlich ist dieses Verhalten genau so gewünscht, auch wenn viele Issues und Feature-Requests bemängeln, dass dieses Verhalten nicht definierbar ist. Gerade wenn wir solche Resources als globalen shared State in einem Service definieren wollen, ist dieses Verhalten ggf. nicht optimal. Wer die Dokumentation genau liest, findet aber ein Schlupfloch, das implizit schon in unserem zweiten Beispiel oben zum Zuge kam: wird „undefined“ als Parameter der Resource übergeben, bleibt diese im „idle“ state und führt keine Aktion aus. Eine Service-Implementierung für eine globale Ressource könnte nun also so aussehen:
@Injectable({
providedIn: 'root'
})
export class ResourceService {
http = inject(HttpClient);
// signal für die Aktivierung der Resource
#todosResourceIdle = signal<boolean>(true)
// Resource-Definition, "params" evaluiert zu "undefined" wenn nicht aktiviert = keine Anfrage
#todosResource = resource<any[], boolean | undefined>({
defaultValue: [],
params: () => this.#todosResourceIdle() ? undefined : true,
loader: () => firstValueFrom(this.http.get<any>('https://dummyjson.com/todos')
.pipe(map(r => r.todos))
)
})
// Zugriff für die Komponenten und Trigger setzen
loadData() {
this.#todosResourceIdle.set(false);
return this.#todosResource;
}
@Component({
...
})
export class DemoComponente{
service = inject(ResourceService);
todoResource = this.service.loadData();
Bleibt abzuwarten, ob das Angular Team uns vielleicht doch noch ein „active“ Flag zur Verfügung stellt, um ein solches Verhalten eleganter zu implementieren. Davon abgesehen ist die ResourceAPI heute schon eine tolle Möglichkeit unsere Rest-Aufrufe in der Signal-Welt zu integrieren.







