Vor resource() sah Async-Loading in Angular immer gleich aus: drei Felder pro Request — loading, error, data — manuell synchron gehalten, dazu ein subscribe mit takeUntilDestroyed und im Template eine *ngIf="loading"/*ngIf="error"/*ngIf="data"-Kette. Bei Param-Wechsel das alte Subscribe canceln, neues starten — Boilerplate, der sich in jeder Komponente wiederholt.
Ab Angular 19 (in v20/v21 weiter stabilisiert) gibt es eine signal-native Antwort darauf: resource() und sein RxJS-Pendant rxResource(). Beide liefern ein ResourceRef mit den Signals value(), status(), error(), isLoading() und einer reload()-Methode. Die Funktion lädt automatisch neu, wenn sich getrackte Signal-Quellen ändern, und cancelt laufende Requests bei Param-Wechsel oder Destroy. Dieser Artikel zeigt die volle API, einen kompletten Edit-Save-Reload-Flow und wie resource() mit linkedSignal für lokal editierbare Felder zusammenspielt.
Async-Loading als Signal-API
resource() aus @angular/core ist eine Funktion, die einen Loader an einen reaktiven Parameter koppelt. Sie gibt ein ResourceRef<T> zurück, das den aktuellen Lade-Zustand über mehrere Signals exponiert:
| Signal | Typ | Bedeutung |
|---|---|---|
value() | Signal<T> oder Signal<T | undefined> | Letzter erfolgreich geladener Wert |
status() | Signal<'idle' | 'loading' | 'reloading' | 'resolved' | 'error' | 'local'> | Aktueller Lade-Status |
error() | Signal<unknown> | Letzter Loader-Fehler oder undefined |
isLoading() | Signal<boolean> | true während loading oder reloading |
hasValue() | Signal<boolean> | true, wenn value() schon einmal gesetzt wurde |
reload() | Methode | Triggert expliziten Reload mit aktuellen Params |
set(...) | Methode | Setzt einen lokalen Wert (Status 'local') |
Statt drei Felder von Hand zu pflegen, hast du eine Resource und im Template @if (r.isLoading()) { ... }-Branches. Refactoring von „klassisch” zu Resource ist meistens ein Fünf-Zeilen-Job pro Komponente.
Params-Funktion + async Loader
Die einfachste Form: Ein params-Signal-Reader und ein loader, der den Param entgegennimmt und einen Promise<T> (oder T) zurückgibt.
import { Component, resource, signal } from '@angular/core';
interface User { id: string; name: string; email: string; }
@Component({ selector: 'app-user-card', standalone: true, template: `
<input [value]="userId()" (input)="userId.set($any($event.target).value)" />
@if (userResource.isLoading()) { <p>Lade ...</p> }
@else if (userResource.error()) { <p>Fehler beim Laden</p> }
@else if (userResource.value(); as u) {
<h2>{{ u.name }}</h2>
<p>{{ u.email }}</p>
}
` })
export class UserCardComponent {
userId = signal('u-1');
userResource = resource({
params: () => this.userId(),
loader: async ({ params, abortSignal }) => {
const res = await fetch(`/api/users/${params}`, { signal: abortSignal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<User>;
},
});
}Die params-Funktion ist eine reaktive Quelle wie bei computed(): Sie liest Signals, alle gelesenen werden tracked. Sobald sich einer ändert, läuft die Funktion erneut, der neue Wert wird mit dem letzten verglichen, und bei Unterschied wird der Loader aufgerufen. Der vorherige Request wird über abortSignal gecancelt.
Saubere Branches statt verschachtelter Checks
Mit den Status-Signals kannst du im Template eine eindeutige Hierarchie bauen — ohne *ngIf-Verschachtelung mit Race-Conditions:
@if (userResource.isLoading() && !userResource.hasValue()) {
<p>Lade User ...</p>
} @else if (userResource.error(); as err) {
<p class="error">{{ asError(err).message }}</p>
<button (click)="userResource.reload()">Erneut versuchen</button>
} @else if (userResource.value(); as user) {
<article [class.refreshing]="userResource.isLoading()">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button (click)="userResource.reload()">Aktualisieren</button>
</article>
}Beachte das isLoading() && !hasValue()-Pattern: Beim ersten Load gibt es noch keinen Wert, dann zeigst du die Lade-Anzeige. Bei einem Reload ist hasValue() true und isLoading() ebenfalls — du zeigst weiter den alten Wert (kein Flackern) und ergänzt eine refreshing-Class für eine subtile UI-Indikation.
Wenn der Loader ein Observable returned
HttpClient gibt Observables zurück — und genau dafür gibt es rxResource() aus @angular/core/rxjs-interop. Es funktioniert wie resource(), akzeptiert aber einen Observable-Loader statt einer async-Funktion.
import { Component, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
interface User { id: string; name: string; email: string; }
@Component({ selector: 'app-user-card', standalone: true, template: `
@if (userResource.isLoading()) { <p>Lade ...</p> }
@else if (userResource.value(); as u) {
<h2>{{ u.name }}</h2>
<p>{{ u.email }}</p>
}
` })
export class UserCardComponent {
private http = inject(HttpClient);
userId = signal('u-1');
userResource = rxResource({
params: () => this.userId(),
stream: ({ params }) =>
this.http.get<User>(`/api/users/${params}`),
});
}Vorteile gegenüber resource():
- Du kannst die volle RxJS-Operator-Pipeline im Loader nutzen (
map,retry,tap,catchError). - Bei Param-Wechsel macht
rxResourceintern einswitchMap— der alte Stream wird automatisch unsubscribed, der neue gestartet. - Direkte Integration mit
HttpClient, ohnefirstValueFrom()-Wrapping.
Was passiert beim schnellen Wechsel?
Eines der wichtigsten Features: Wenn sich params() ändert, während ein Load läuft, wird der laufende Request abgebrochen. Das verhindert Race-Conditions, bei denen der alte Request später ankommt und einen veralteten Wert in den State schreibt.
Bei resource() geschieht der Abbruch über das abortSignal-Argument im Loader-Context — du musst es an fetch durchreichen:
import { resource, signal } from '@angular/core';
const userId = signal('u-1');
const r = resource({
params: () => userId(),
loader: async ({ params, abortSignal }) => {
// abortSignal wird ausgeloest, sobald params() neu emittiert
// oder die Komponente zerstoert wird
const res = await fetch(`/api/users/${params}`, { signal: abortSignal });
return res.json();
},
});
userId.set('u-2'); // bricht den laufenden u-1-Request abBei rxResource() ist der Abbruch eingebaut: Das interne switchMap unsubscribed den alten Stream automatisch — HttpClient-Requests cancelt das auf XHR-Ebene.
reload() für expliziten Refresh
Manchmal brauchst du einen Reload, ohne dass sich params ändert — typisch nach einem Save-Request, einem Pull-to-Refresh oder einem manuellen „Aktualisieren”-Button. Dafür gibt es .reload().
async save(payload: Partial<User>) {
await fetch(`/api/users/${this.userId()}`, {
method: 'PATCH',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
});
// Server-State frisch ziehen
this.userResource.reload();
}Wichtig: Während des Reloads bleibt value() auf dem letzten Wert, status() wird 'reloading' und isLoading() true. Im Template kannst du das nutzen, um „die Daten sind noch da, aber wir laden frisch nach” als UI-State zu zeigen — ohne Flackern.
Komplett durchdachter Flow mit linkedSignal als Brücke
Edit-Felder dürfen nicht direkt auf value() schreiben — value() ist read-only und kommt vom Loader. Stattdessen nutzt du linkedSignal als lokalen Edit-State, der beim Reload automatisch resettet.
import { Component, inject, linkedSignal, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
interface User { id: string; name: string; email: string; }
@Component({ selector: 'app-profile-edit', standalone: true, template: `
@if (profile.isLoading() && !profile.hasValue()) {
<p>Lade ...</p>
} @else if (profile.value(); as user) {
<form (submit)="save($event)">
<label>Name
<input [value]="draftName()"
(input)="draftName.set($any($event.target).value)" />
</label>
<label>E-Mail
<input [value]="draftEmail()"
(input)="draftEmail.set($any($event.target).value)" />
</label>
<button type="submit" [disabled]="saving()">
{{ saving() ? 'Speichere ...' : 'Speichern' }}
</button>
</form>
}
` })
export class ProfileEditComponent {
private http = inject(HttpClient);
userId = signal('u-1');
saving = signal(false);
profile = rxResource<User, string>({
params: () => this.userId(),
stream: ({ params }) =>
this.http.get<User>(`/api/users/${params}`),
});
// linkedSignal als Edit-Bruecke: bei profile.reload() wird der Wert frisch geseeded
draftName = linkedSignal(() => this.profile.value()?.name ?? '');
draftEmail = linkedSignal(() => this.profile.value()?.email ?? '');
async save(ev: Event) {
ev.preventDefault();
this.saving.set(true);
try {
await fetch(`/api/users/${this.userId()}`, {
method: 'PATCH',
body: JSON.stringify({
name: this.draftName(),
email: this.draftEmail(),
}),
headers: { 'Content-Type': 'application/json' },
});
this.profile.reload(); // -> draftName/draftEmail werden via linkedSignal neu seeded
} finally {
this.saving.set(false);
}
}
}Das ist eine vollständige, idiomatische Edit-Komponente in rund 30 Zeilen Logik — ohne klassisches loading/error/data-Triple, ohne manuelles Subscribe, ohne ngOnDestroy, ohne Race-Conditions bei schnellem User-Wechsel.
Was bedeutet welcher Status?
| Status | Wann | isLoading() | value() |
|---|---|---|---|
'idle' | Resource wartet — params returned undefined | false | undefined (oder Default) |
'loading' | Erster Load läuft | true | undefined (oder Default) |
'reloading' | Folge-Load läuft (Param-Update oder reload()) | true | letzter Wert (kein Flackern) |
'resolved' | Load erfolgreich abgeschlossen | false | aktueller Wert |
'error' | Loader hat geworfen | false | letzter Wert oder Default |
'local' | set(...) wurde manuell aufgerufen | false | manuell gesetzter Wert |
Der Status 'idle' ist nützlich, um „kein User ausgewählt — kein Load nötig” auszudrücken. Wenn params() undefined zurückgibt, läuft der Loader nicht, das Signal bleibt im Idle-Zustand. So vermeidest du unnötige Requests vor User-Auswahl.
import { resource, signal } from '@angular/core';
const selectedId = signal<string | null>(null);
const r = resource({
// Returnt undefined -> Resource bleibt im Status 'idle', kein Loader-Aufruf
params: () => selectedId() ?? undefined,
loader: async ({ params }) =>
fetch(`/api/items/${params}`).then(res => res.json()),
});Drei Wege, ein User zu laden
| Aspekt | resource() / rxResource() | async-Pipe + HttpClient | manuelles subscribe() |
|---|---|---|---|
| Auto-Cancel bei Param-Wechsel | Ja | Nein (manuell mit switchMap) | Nein |
| Auto-Cleanup bei Destroy | Ja | Ja (Pipe) | Nein (takeUntilDestroyed!) |
Status-Signals (isLoading, error) | Eingebaut | Selbst aufbauen | Selbst aufbauen |
reload() | Eingebaut | Trigger-Subject manuell | Manuell |
| Boilerplate | Niedrig | Mittel | Hoch |
| RxJS-Operator-Power | Mit rxResource | Voll | Voll |
| Mutations (POST/PATCH) | Nicht empfohlen | Möglich | Möglich |
Resource ist klar das richtige Werkzeug für read-only Loads mit reaktivem Param. Für Mutations bleibst du bei einem Service mit HttpClient und triggerst nach dem Save ein resource.reload().
Besonderheiten
Stand v21 weiter stabilisiert, aber Detail-API kann sich erweitern
Die Kern-Funktion resource() ist als API stabil, einzelne Optionen (z. B. defaultValue, equal, neue Status-Werte) werden noch ergänzt. Im Produktionscode kein Problem — bleib bei den dokumentierten Optionen, dann bist du auf der sicheren Seite.
params-Funktion läuft wie computed
Alle gelesenen Signals werden automatisch tracked. params: () => this.userId() + ‘?lang=’ + this.lang() reagiert auf beide Quellen. Wenn sich nichts ändert, läuft der Loader nicht — das ist günstig im Render-Zyklus.
params undefined verhindert Loading
Wenn deine params-Funktion undefined zurückgibt, bleibt die Resource im Status ‘idle’ und der Loader wird nicht aufgerufen. Ideal für „kein User ausgewählt” — du sparst dir einen @if-Wrapper um die ganze Resource-Definition.
AbortSignal im Loader für native fetch
loader: async ({ params, abortSignal }) => fetch(url, { signal: abortSignal }) ist das idiomatische Muster mit nativen Fetch-APIs. Bei Param-Wechsel oder Destroy wird abortSignal ausgelöst, der Request abgebrochen — kein Race-Condition-Risiko.
rxResource macht intern switchMap
Bei jedem Param-Update wird der alte Stream unsubscribed und der neue gestartet — exakt das Verhalten von switchMap. Für Mutations ist das gefährlich, für Reads ideal: User wechselt schnell zwischen Datensätzen, nur das letzte Result landet im State.
value() ist Read-only — Brücke über linkedSignal
Du kannst value() nicht direkt editieren. Für editierbare Felder nutzt du linkedSignal(() => resource.value()?.feldname) als lokalen Draft-State. Beim reload() wird der Draft automatisch neu seeded, weil sich die Source ändert.
Bei Loader-Error bleibt der letzte Wert in value()
Der Status wird ‘error’, error() liefert das Throwable, aber value() behält den letzten erfolgreichen Wert. Im Template immer den Status prüfen, bevor du value() renderst — sonst zeigst du veraltete Daten ohne Hinweis auf den Fehler.
Reload behält value() während des Reloads
status() wird ‘reloading’, isLoading() ist true, aber value() bleibt auf dem letzten Wert stehen. Ideal für Pull-to-Refresh oder „Aktualisieren”-Buttons ohne Flackern — du blendest nur einen subtilen Spinner ein, der Inhalt bleibt sichtbar.
Weiterführende Ressourcen
Externe Quellen
- resource API – Angular.dev
- rxResource API – Angular.dev
- Signals Guide – Angular.dev
- RxJS Interop Guide – Angular.dev