Eine Resource verwandelt eine asynchrone Operation (meist einen HTTP-Request) in eine Reihe von Signals: value(), status(), error(), isLoading(). Du gibst ihr reaktive Parameter; ändern sie sich, lädt die Resource von selbst neu und bricht automatisch den vorherigen Request ab. Ab v22 sind resource, rxResource und httpResource stabil.
- Die APIs
resource,rxResourceundhttpResourcesind nicht mehr experimentell. - Du kannst
ResourceParamsStatus.IDLE/LOADINGausparamswerfen. paramserhält einen Kontext mitchain()für abhängige Resources.- Streams können auch synchrone Werte haben; Resources können beim SSR gecacht werden.
Die drei Varianten
| Funktion | Dein Loader liefert | Wann du sie wählst |
|---|---|---|
httpResource() | — (sie stellt den Request selbst über HttpClient) | Der häufigste Fall: ein einfaches GET. Läuft durch die Interceptors. |
resource() | Promise | Beliebige asynchrone Arbeit: fetch, IndexedDB, SDKs, Web Workers. |
rxResource() | Observable | Du hast bereits RxJS-Services oder -Streams (WebSocket). |
resource(): Aufbau
import { Component, resource, signal } from '@angular/core';
@Component({ /* ... */ })
export class UserProfile {
readonly userId = signal<number | undefined>(1);
readonly user = resource({
// 1. params: eine reaktive Funktion. Ändert sich das Ergebnis → wird neu geladen.
// Liefert sie undefined → Status 'idle', der Loader läuft NICHT.
params: () => this.userId(),
// 2. loader: erhält params + ein AbortSignal (der alte Request wird automatisch abgebrochen)
loader: async ({ params: id, abortSignal }) => {
const res = await fetch(`/api/users/${id}`, { signal: abortSignal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as User;
},
});
}- ✓ aufgelöst #1
- → Start #1
- Klicke schnell auf #2, #3, #4 — die alten Requests werden abgebrochen.
Die vollständige API einer Resource
user.value(); // Signal<T | undefined> — der letzte Wert (oder defaultValue)
user.hasValue(); // Type Guard: true, wenn value() nicht undefined ist
user.status(); // 'idle' | 'loading' | 'reloading' | 'resolved' | 'error' | 'local'
user.isLoading(); // true für 'loading' und 'reloading'
user.error(); // Error | undefined
user.reload(); // lädt mit denselben params neu (Status 'reloading', value bleibt)
// Lokales Schreiben (z. B. ein optimistisches Update) → Status wird 'local'
user.set(newUser);
user.update((u) => ({ ...u!, name: 'Neu' }));
user.asReadonly(); // ohne set/update bereitgestellt
user.snapshot(); // v21.2: { status, value | error } in einem einzigen SignalhttpResource(): reaktive GETs in zwei Zeilen
httpResource nutzt im Hintergrund HttpClient, daher funktionieren deine Interceptors (Auth, Logging, Mock). Du übergibst eine Funktion, die die URL oder ein Request-Objekt zurückgibt; jedes dort gelesene Signal wird zu einer Abhängigkeit.
import { httpResource } from '@angular/common/http';
export class UserSearch {
readonly query = signal('');
readonly role = signal<Role | ''>('');
// Einfache Form: eine Funktion, die die URL liefert (undefined → idle)
readonly user = httpResource<UserDto>(() => `/api/users/${this.userId()}`);
// Vollständige Form: ein Request-Objekt + Optionen
readonly users = httpResource<PageDto<UserDto>>(
() => ({
url: '/api/users',
params: { q: this.query(), ...(this.role() ? { role: this.role() } : {}) },
headers: { 'X-Client': 'atlas' },
}),
{ defaultValue: { items: [], total: 0 } }, // value() ist nie mehr undefined
);
}
// Andere Formate:
httpResource.text(() => '/api/readme');
httpResource.blob(() => `/api/files/${id()}`);
httpResource.arrayBuffer(() => '/api/binary');- Ana Popescu· Cluj-Napocaadmin
- Mihai Ionescu· Bucureștieditor
- Ioana Dumitru· Iașiviewer
- Radu Stan· Timișoaraeditor
- Elena Marin· Brașovviewer
- Andrei Georgescu· Cluj-Napocaadmin
- Maria Constantin· Constanțaviewer
- Vlad Petrescu· Sibiueditor
- Cristina Pop· Oradeaviewer
- Bogdan Rusu· Iașiviewer
Validierung und Mapping mit parse
Die Option parse läuft auf der Response, bevor sie value() erreicht. Sie ist der perfekte Ort für ein Validierungsschema (Zod, Valibot) oder für das Mapping DTO → Modell (siehe Interfaces & Modelle).
import { z } from 'zod';
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.email() });
// `parse` validiert/transformiert die Response — der Fehler landet in user.error()
readonly user = httpResource(() => `/api/users/${this.id()}`, {
parse: UserSchema.parse,
});
// oder Mapping DTO → Domänenmodell
readonly product = httpResource(() => `/api/products/${this.id()}`, {
parse: (dto: ProductDto) => toProduct(dto),
});rxResource(): wenn du schon Observables hast
import { rxResource } from '@angular/core/rxjs-interop';
export class Orders {
private readonly api = inject(OrdersApi); // liefert Observables
readonly status = signal<OrderStatus>('open');
readonly orders = rxResource({
params: () => ({ status: this.status() }),
stream: ({ params }) => this.api.list(params.status), // Observable<Order[]>
defaultValue: [],
});
}
// Ändern sich die params, wird das alte Abonnement geschlossen (wie bei switchMap).
// Ein Stream kann mehrfach emittieren (z. B. ein WebSocket) — value() wird bei jeder Emission aktualisiert.Spezielle Status und verkettete Resources v22
import { ResourceParamsStatus, resource } from '@angular/core';
// v22: In params kannst du einen speziellen Status „werfen“
readonly report = resource({
params: () => {
const range = this.range();
if (!range) throw ResourceParamsStatus.IDLE; // noch nichts zu laden
if (!this.permissions.hasValue()) throw ResourceParamsStatus.LOADING; // wartet auf etwas anderes
return range;
},
loader: ({ params }) => this.api.report(params),
});
// Resources verketten mit ctx.chain(): orders wartet auf user
readonly user = httpResource<User>(() => '/api/me');
readonly orders = httpResource<Order[]>((ctx) => {
const user = ctx.chain(this.user);
return user ? `/api/users/${user.id}/orders` : undefined;
});
// solange user lädt → ist orders 'loading'; wenn user einen Fehler hat → hat orders ebenfalls einenuserresolvedAna Popescu · Cluj-Napoca
sameCityresolvedAna Popescu, Andrei Georgescu
readonly user = httpResource<User>(() =>
this.userId() ? `/api/users/${this.userId()}` : undefined,
);
readonly sameCity = httpResource<Page<User>>((ctx) => {
const user = ctx.chain(this.user);
return user ? `/api/users?q=${user.city}` : undefined;
});Status, wie ein Effect sie sieht
- 3 ms user: loading · sameCity: loading
- 467 ms user: resolved · sameCity: loading
- 1234 ms user: resolved · sameCity: resolved
Schreiben: Resource ist nichts für POST
Resources sind zum Lesen gedacht. Eine Resource führt ihren Loader erneut aus, wenn sich ihre Parameter ändern — genau das willst du bei einem „Speichern“-Befehl NICHT. Für Mutationen nutzt du HttpClient und aktualisierst danach die Resource lokal (update) oder lädst sie neu (reload).
// resource / httpResource sind zum LESEN (GET) gedacht.
// Zum Schreiben (POST/PUT/DELETE) nutzt du HttpClient direkt und aktualisierst danach die Resource.
export class TodoStore {
private readonly http = inject(HttpClient);
readonly todos = httpResource<Todo[]>(() => '/api/todos', { defaultValue: [] });
async add(title: string) {
const created = await firstValueFrom(this.http.post<Todo>('/api/todos', { title }));
this.todos.update((list) => [...list, created]); // lokales Update, kein erneutes Laden
}
async remove(id: number) {
this.todos.update((list) => list.filter((t) => t.id !== id)); // optimistisch
try {
await firstValueFrom(this.http.delete(`/api/todos/${id}`));
} catch {
this.todos.reload(); // einfacher Rollback: vom Server neu laden
}
}
}Wann ja, wann nein
- Daten, die vom Zustand abhängen: Details per
id, Listen nach Filtern, Paginierung. - Jeder Screen mit „Lädt / Fehler / Daten“.
- Ein Ersatz für
toSignal(this.route.params.pipe(switchMap(...))). - Signal-Stores, die Daten vom Server brauchen.
- POST / PUT / DELETE →
HttpClient. - Komplexe Abläufe mit Retry/Backoff, Polling, Stream-Kombinationen → direkt RxJS (oder
rxResourcedarauf aufgesetzt). - Statische Daten, die einmal beim Start geladen werden →
provideAppInitializeroder ein Resolver.
Häufige Stolperfallen
value()ist am Anfangundefinedund während des ersten Ladens. NutzehasValue()oderdefaultValue.- „Verschluckte“ Fehler: Wenn du
error()nicht anzeigst, sieht der Benutzer nur eine leere Liste. Behandle den Fehlerzustand explizit. value()zu lesen, wenn der Statuserrorist, wirft den Fehler — prüfe vorhererror()oderhasValue().- Params, die sich bei jedem Tastendruck ändern: Jeder Tastendruck = ein Request (der alte wird abgebrochen). Kombiniere das für die Suche mit debounced().
- Neue Objekte in params:
params: () => ({ id: this.id() })ist in Ordnung — die Resource vergleicht das Ergebnis und lädt nur neu, wenn sich die gelesenen Signals ändern.
Spickzettel
resource({ params: () => p(), loader: async ({ params, abortSignal, previous }) => ... });
rxResource({ params: () => p(), stream: ({ params }) => obs$ });
httpResource<T>(() => url | { url, method, params, headers, body } | undefined, { defaultValue, parse });
r.value() r.hasValue() r.status() r.isLoading() r.error() r.reload() r.set() r.update()
params → undefined ⇒ 'idle'
params → throw ResourceParamsStatus.IDLE / LOADING (v22)
(ctx) => ctx.chain(other) ⇒ Verkettung (v22)