O resursă transformă o operație asincronă (de obicei un request HTTP) într-un set de semnale: value(), status(), error(), isLoading(). Îi dai parametri reactivi; când aceștia se schimbă, resursa reîncarcă singură și anulează automat cererea precedentă. Din v22, resource, rxResource și httpResource sunt stabile.
- API-urile
resource,rxResource,httpResourceau ieșit din experimental. - Poți arunca
ResourceParamsStatus.IDLE/LOADINGdinparams. paramsprimește un context cuchain()pentru resurse dependente.- Stream-urile pot avea și valori sincrone; resursele pot fi puse în cache la SSR.
Cele trei variante
| Funcție | Loader-ul tău întoarce | Când o alegi |
|---|---|---|
httpResource() | — (face el request-ul prin HttpClient) | Cazul cel mai des: GET simplu. Trece prin interceptori. |
resource() | Promise | Orice async: fetch, IndexedDB, SDK-uri, Web Workers. |
rxResource() | Observable | Ai deja servicii pe RxJS sau stream-uri (websocket). |
resource(): anatomia
import { Component, resource, signal } from '@angular/core';
@Component({ /* ... */ })
export class UserProfile {
readonly userId = signal<number | undefined>(1);
readonly user = resource({
// 1. params: o funcție reactivă. Când rezultatul se schimbă → se reîncarcă.
// Dacă întoarce undefined → status 'idle', loader-ul NU rulează.
params: () => this.userId(),
// 2. loader: primește params + un AbortSignal (cererea veche e anulată automat)
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;
},
});
}- ✓ rezolvat #1
- → start #1
- Apasă rapid #2, #3, #4 — cererile vechi sunt anulate.
API-ul complet al unei resurse
user.value(); // Signal<T | undefined> — ultima valoare (sau defaultValue)
user.hasValue(); // type guard: true dacă value() nu e undefined
user.status(); // 'idle' | 'loading' | 'reloading' | 'resolved' | 'error' | 'local'
user.isLoading(); // true pentru 'loading' și 'reloading'
user.error(); // Error | undefined
user.reload(); // reîncarcă cu aceiași params (status 'reloading', value rămâne)
// Scriere locală (ex. update optimist) → status devine 'local'
user.set(newUser);
user.update((u) => ({ ...u!, name: 'Nou' }));
user.asReadonly(); // expui fără set/update
user.snapshot(); // v21.2: { status, value | error } într-un singur semnalhttpResource(): GET-uri reactive în două rânduri
httpResource folosește HttpClient pe dedesubt, deci interceptorii tăi funcționează (auth, logging, mock). Îi dai o funcție care întoarce URL-ul sau un obiect de request; fiecare semnal citit acolo devine dependență.
import { httpResource } from '@angular/common/http';
export class UserSearch {
readonly query = signal('');
readonly role = signal<Role | ''>('');
// Forma simplă: funcție care întoarce URL-ul (undefined → idle)
readonly user = httpResource<UserDto>(() => `/api/users/${this.userId()}`);
// Forma completă: obiect de request + opțiuni
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() nu mai e niciodată undefined
);
}
// Alte 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
Validare și mapare cu parse
Opțiunea parse rulează pe răspuns înainte să ajungă în value(). E locul perfect pentru o schemă de validare (Zod, Valibot) sau pentru maparea DTO → model (vezi Interfețe & modele).
import { z } from 'zod';
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.email() });
// `parse` validează/transformă răspunsul — eroarea ajunge în user.error()
readonly user = httpResource(() => `/api/users/${this.id()}`, {
parse: UserSchema.parse,
});
// sau mapare DTO → model de domeniu
readonly product = httpResource(() => `/api/products/${this.id()}`, {
parse: (dto: ProductDto) => toProduct(dto),
});rxResource(): când ai deja Observable-uri
import { rxResource } from '@angular/core/rxjs-interop';
export class Orders {
private readonly api = inject(OrdersApi); // întoarce Observable-uri
readonly status = signal<OrderStatus>('open');
readonly orders = rxResource({
params: () => ({ status: this.status() }),
stream: ({ params }) => this.api.list(params.status), // Observable<Order[]>
defaultValue: [],
});
}
// La schimbarea params, abonarea veche e închisă (ca switchMap).
// Un stream poate emite de mai multe ori (ex. websocket) — value() se actualizează la fiecare emisie.Status-uri speciale și resurse înlănțuite v22
import { ResourceParamsStatus, resource } from '@angular/core';
// v22: în params poți „arunca” un status special
readonly report = resource({
params: () => {
const range = this.range();
if (!range) throw ResourceParamsStatus.IDLE; // nimic de încărcat încă
if (!this.permissions.hasValue()) throw ResourceParamsStatus.LOADING; // așteaptă altceva
return range;
},
loader: ({ params }) => this.api.report(params),
});
// Înlănțuire de resurse cu ctx.chain(): orders așteaptă 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;
});
// cât timp user se încarcă → orders e 'loading'; dacă user dă eroare → și orders e în eroareuserresolvedAna 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;
});Stările, așa cum le vede un effect
- 11 ms user: loading · sameCity: loading
- 719 ms user: resolved · sameCity: loading
- 1258 ms user: resolved · sameCity: resolved
Scrieri: resource nu e pentru POST
Resursele sunt gândite pentru citire. O resursă re-execută loader-ul când se schimbă parametrii — exact ce NU vrei pentru o comandă „salvează”. Pentru mutații folosește HttpClient și apoi actualizează resursa local (update) sau reîncarc-o (reload).
// resource / httpResource sunt pentru CITIRE (GET).
// Pentru scrieri (POST/PUT/DELETE) folosește HttpClient direct, apoi actualizează resursa.
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]); // update local, fără refetch
}
async remove(id: number) {
this.todos.update((list) => list.filter((t) => t.id !== id)); // optimist
try {
await firstValueFrom(this.http.delete(`/api/todos/${id}`));
} catch {
this.todos.reload(); // rollback simplu: reîncarci de pe server
}
}
}Când DA, când NU
- Date care depind de stare: detalii după
id, liste după filtre, paginare. - Orice ecran cu „loading / eroare / date”.
- Înlocuitor pentru
toSignal(this.route.params.pipe(switchMap(...))). - Store-uri pe semnale care au nevoie de date de la server.
- POST / PUT / DELETE →
HttpClient. - Fluxuri complexe cu retry/backoff, polling, combinări de stream-uri → RxJS direct (sau
rxResourcepeste el). - Date statice, încărcate o singură dată la pornire →
provideAppInitializersau un resolver.
Capcane frecvente
value()eundefinedla început și în timpul încărcării inițiale. FoloseștehasValue()saudefaultValue.- Erori „înghițite”: dacă nu afișezi
error(), utilizatorul vede doar o listă goală. Tratează explicit starea de eroare. - Citirea lui
value()când status eerroraruncă eroarea — verifică întâierror()sauhasValue(). - Params care se schimbă la fiecare tastă: fiecare tastă = un request (cel vechi e anulat). Pentru căutare, combină cu debounced().
- Obiecte noi în params:
params: () => ({ id: this.id() })e ok — resursa compară rezultatul și reîncarcă doar când semnalele citite se schimbă.
Fișă rapidă
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(alta) ⇒ înlănțuire (v22)