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:

SignalTypBedeutung
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()MethodeTriggert expliziten Reload mit aktuellen Params
set(...)MethodeSetzt 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.

TypeScript user-resource.ts
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:

HTML resource-template.html
@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.

TypeScript user-rxresource.ts
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 rxResource intern ein switchMap — der alte Stream wird automatisch unsubscribed, der neue gestartet.
  • Direkte Integration mit HttpClient, ohne firstValueFrom()-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:

TypeScript abort-signal.ts
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 ab

Bei 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().

TypeScript reload-after-save.ts
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.

TypeScript profile-edit.component.ts
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?

StatusWannisLoading()value()
'idle'Resource wartet — params returned undefinedfalseundefined (oder Default)
'loading'Erster Load läufttrueundefined (oder Default)
'reloading'Folge-Load läuft (Param-Update oder reload())trueletzter Wert (kein Flackern)
'resolved'Load erfolgreich abgeschlossenfalseaktueller Wert
'error'Loader hat geworfenfalseletzter Wert oder Default
'local'set(...) wurde manuell aufgerufenfalsemanuell 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.

TypeScript conditional-load.ts
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

Aspektresource() / rxResource()async-Pipe + HttpClientmanuelles subscribe()
Auto-Cancel bei Param-WechselJaNein (manuell mit switchMap)Nein
Auto-Cleanup bei DestroyJaJa (Pipe)Nein (takeUntilDestroyed!)
Status-Signals (isLoading, error)EingebautSelbst aufbauenSelbst aufbauen
reload()EingebautTrigger-Subject manuellManuell
BoilerplateNiedrigMittelHoch
RxJS-Operator-PowerMit rxResourceVollVoll
Mutations (POST/PATCH)Nicht empfohlenMöglichMö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

/ Weiter

Zurück zu Signals

Zur Übersicht