Une resource transforme une opération asynchrone (en général une requête HTTP) en un ensemble de Signals : value(), status(), error(), isLoading(). Vous lui donnez des paramètres réactifs ; quand ils changent, la resource se recharge toute seule et annule automatiquement la requête précédente. Depuis la v22, resource, rxResource et httpResource sont stables.
- Les API
resource,rxResourceethttpResourcene sont plus expérimentales. - Vous pouvez lancer
ResourceParamsStatus.IDLE/LOADINGdepuisparams. paramsreçoit un contexte avecchain()pour les resources dépendantes.- Les flux peuvent aussi avoir des valeurs synchrones ; les resources peuvent être mises en cache pendant le SSR.
Les trois variantes
| Fonction | Votre loader renvoie | Quand la choisir |
|---|---|---|
httpResource() | — (elle fait la requête elle-même via HttpClient) | Le cas le plus courant : un simple GET. Passe par les interceptors. |
resource() | Promise | Tout travail asynchrone : fetch, IndexedDB, SDK, Web Workers. |
rxResource() | Observable | Vous avez déjà des services ou des flux RxJS (websocket). |
resource() : anatomie
import { Component, resource, signal } from '@angular/core';
@Component({ /* ... */ })
export class UserProfile {
readonly userId = signal<number | undefined>(1);
readonly user = resource({
// 1. params : une fonction réactive. Quand le résultat change → rechargement.
// Si elle renvoie undefined → statut 'idle', le loader ne s’exécute PAS.
params: () => this.userId(),
// 2. loader : reçoit params + un AbortSignal (l’ancienne requête est annulée automatiquement)
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;
},
});
}- ✓ terminée #1
- → début #1
- Appuyez rapidement sur #2, #3, #4 — les anciennes requêtes sont annulées.
L’API complète d’une resource
user.value(); // Signal<T | undefined> — la dernière valeur (ou defaultValue)
user.hasValue(); // type guard : true si value() n’est pas undefined
user.status(); // 'idle' | 'loading' | 'reloading' | 'resolved' | 'error' | 'local'
user.isLoading(); // true pour 'loading' et 'reloading'
user.error(); // Error | undefined
user.reload(); // recharge avec les mêmes params (statut 'reloading', value reste)
// Écriture locale (par ex. mise à jour optimiste) → le statut devient 'local'
user.set(newUser);
user.update((u) => ({ ...u!, name: 'Nouveau' }));
user.asReadonly(); // exposé sans set/update
user.snapshot(); // v21.2 : { status, value | error } dans un seul signalhttpResource() : des GET réactifs en deux lignes
httpResource utilise HttpClient en interne, donc vos interceptors fonctionnent (auth, logs, mock). Vous lui donnez une fonction qui renvoie l’URL ou un objet de requête ; chaque signal lu à cet endroit devient une dépendance.
import { httpResource } from '@angular/common/http';
export class UserSearch {
readonly query = signal('');
readonly role = signal<Role | ''>('');
// Forme simple : une fonction qui renvoie l’URL (undefined → idle)
readonly user = httpResource<UserDto>(() => `/api/users/${this.userId()}`);
// Forme complète : un objet de requête + des options
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() n’est plus jamais undefined
);
}
// Autres formats :
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
Validation et mapping avec parse
L’option parse s’exécute sur la réponse avant qu’elle n’atteigne value(). C’est l’endroit idéal pour un schéma de validation (Zod, Valibot) ou pour mapper un DTO → modèle (voir Interfaces et modèles).
import { z } from 'zod';
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.email() });
// `parse` valide/transforme la réponse — l’erreur se retrouve dans user.error()
readonly user = httpResource(() => `/api/users/${this.id()}`, {
parse: UserSchema.parse,
});
// ou mapping d’un DTO → modèle du domaine
readonly product = httpResource(() => `/api/products/${this.id()}`, {
parse: (dto: ProductDto) => toProduct(dto),
});rxResource() : quand vous avez déjà des Observables
import { rxResource } from '@angular/core/rxjs-interop';
export class Orders {
private readonly api = inject(OrdersApi); // renvoie des Observables
readonly status = signal<OrderStatus>('open');
readonly orders = rxResource({
params: () => ({ status: this.status() }),
stream: ({ params }) => this.api.list(params.status), // Observable<Order[]>
defaultValue: [],
});
}
// Quand params change, l’ancien abonnement est fermé (comme switchMap).
// Un flux peut émettre plusieurs fois (par ex. un websocket) — value() se met à jour à chaque émission.Statuts spéciaux et resources chaînées v22
import { ResourceParamsStatus, resource } from '@angular/core';
// v22 : dans params, vous pouvez « lancer » un statut spécial
readonly report = resource({
params: () => {
const range = this.range();
if (!range) throw ResourceParamsStatus.IDLE; // rien à charger pour l’instant
if (!this.permissions.hasValue()) throw ResourceParamsStatus.LOADING; // en attente d’autre chose
return range;
},
loader: ({ params }) => this.api.report(params),
});
// Chaînage de resources avec ctx.chain() : orders attend 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;
});
// tant que user charge → orders est 'loading' ; si user est en erreur → orders aussiuserresolvedAna 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;
});Les statuts, tels qu’un effet les voit
- 5 ms user: loading · sameCity: loading
- 717 ms user: resolved · sameCity: loading
- 1324 ms user: resolved · sameCity: resolved
Écritures : resource n’est pas faite pour POST
Les resources sont conçues pour la lecture. Une resource relance son loader quand ses paramètres changent — exactement ce que vous ne voulez PAS pour une commande « enregistrer ». Pour les mutations, utilisez HttpClient, puis mettez la resource à jour localement (update) ou rechargez-la (reload).
// resource / httpResource servent à LIRE (GET).
// Pour les écritures (POST/PUT/DELETE), utilisez HttpClient directement, puis mettez la resource à jour.
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]); // mise à jour locale, sans nouvelle requête
}
async remove(id: number) {
this.todos.update((list) => list.filter((t) => t.id !== id)); // optimiste
try {
await firstValueFrom(this.http.delete(`/api/todos/${id}`));
} catch {
this.todos.reload(); // rollback simple : recharger depuis le serveur
}
}
}Quand OUI, quand NON
- Des données qui dépendent de l’état : détails par
id, listes par filtres, pagination. - Tout écran avec « chargement / erreur / données ».
- Un remplaçant de
toSignal(this.route.params.pipe(switchMap(...))). - Les stores à base de Signals qui ont besoin de données du serveur.
- POST / PUT / DELETE →
HttpClient. - Les flux complexes avec retry/backoff, polling, combinaisons de flux → RxJS directement (ou
rxResourcepar-dessus). - Des données statiques, chargées une fois au démarrage →
provideAppInitializerou un resolver.
Pièges fréquents
value()vautundefinedau début et pendant le premier chargement. UtilisezhasValue()oudefaultValue.- Des erreurs « avalées » : si vous n’affichez pas
error(), l’utilisateur ne voit qu’une liste vide. Gérez explicitement l’état d’erreur. - Lire
value()quand le statut esterrorlance l’erreur — vérifiez d’aborderror()ouhasValue(). - Des params qui changent à chaque frappe : chaque frappe = une requête (l’ancienne est annulée). Pour la recherche, combinez avec debounced().
- De nouveaux objets dans params :
params: () => ({ id: this.id() })convient — la resource compare le résultat et ne recharge que si les signals lus changent.
Aide-mémoire
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) ⇒ chaînage (v22)