rxResource (Angular 21)
What is rxResource?
Section intitulée « What is rxResource? »rxResource is a new Angular 21 primitive that simplifies loading data from APIs. It automatiqueally handles loading states, errors, and reactive updates when dependencies change.
Why Use It?
Section intitulée « Why Use It? »- ✅ Automatique loading states - No manual isLoading flags
- ✅ Error handling - Built-in error state
- ✅ Reactive loading - Auau-reloads when dependencies change
- ✅ Cleaner code - Moins de boilerplate than manual HttpClient
- ✅ Basé sur signal - Works seamlessly with signals
rxResource vs HttpClient
Section intitulée « rxResource vs HttpClient »| Feature | HttpClient |
rxResource |
|---|---|---|
| Loading state | Manual | Automatique |
| Error state | Manual try/catch | Automatique |
| Reactive reload | Manual effect/subscribe | Automatique |
| Nettoyage | Manual unsubscribe | Automatique |
| Result type | Observable | Signal |
Basic Utilisation
Section intitulée « Basic Utilisation »import { Component } from '@angular/core';import { rxResource } from '@angular/core/rxjs-interop';import { HttpClient } from '@angular/common/http';
interface User { id: number; name: string; email: string;}
@Component({ selector: 'app-user-list', standalone: true, template: ` <div> <!-- Loading state --> <div *ngIf="users.isLoading()">Loading users...</div>
<!-- Error state --> <div *ngIf="users.error()" class="error"> Error: {{ users.error()?.message }} </div>
<!-- Success state --> <div *ngIf="users.value()"> <div *ngFor="let user of users.value()"> {{ user.name }} - {{ user.email }} </div> </div>
<!-- Status --> <p>Status: {{ users.status() }}</p> </div> `})export class UserListComponent { http = inject(HttpClient);
// Create a resource that loads users users = rxResource({ loader: () => this.http.get<User[]>('/api/users') });
// Accéder à the data: // users.value() - The loaded data (or undefined) // users.isLoading() - Boolean loading state // users.error() - Error object (or null) // users.status() - 'idle' | 'loading' | 'error' | 'success'}What you get automatiqueally:
- ✅
value()- The loaded data - ✅
isLoading()- Loading state - ✅
error()- Error state - ✅
status()- Overall status - ✅
reload()- Method à reload manually
// ❌ Traditional approach - lots of boilerplate@Component({ template: ` <div *ngIf="isLoading">Loading...</div> <div *ngIf="error">Error: {{ error }}</div> <div *ngIf="!isLoading && users"> <div *ngFor="let user of users">{{ user.name }}</div> </div> `})export class UserListComponent { http = inject(HttpClient);
users: User[] | null = null; isLoading = signal(false); error = signal<string | null>(null);
ngOnInit() { this.loadUsers(); }
loadUsers() { this.isLoading.set(true); this.error.set(null);
this.http.get<User[]>('/api/users').pipe( catchError(err => { this.error.set(err.message); return of(null); }), finalize(() => this.isLoading.set(false)) ).subscribe(users => { this.users = users; }); }}// ✅ With rxResource - clean and automatiqueusers = rxResource({ loader: () => this.http.get<User[]>('/api/users')});// All states handled automatiqueally!Reactive Loading with Request
Section intitulée « Reactive Loading with Request »import { Component, signal } from '@angular/core';import { rxResource } from '@angular/core/rxjs-interop';import { HttpClient } from '@angular/common/http';
@Component({ selector: 'app-user-detail', standalone: true, template: ` <div> <input type="number" [(ngModel)]="userId" placeholder="Enter user ID" />
<button (click)="loadUser()">Load User</button>
<div *ngIf="user.isLoading()">Loading user...</div> <div *ngIf="user.error()">Error: {{ user.error()?.message }}</div>
<div *ngIf="user.value()"> <h2>{{ user.value()!.name }}</h2> <p>{{ user.value()!.email }}</p> </div> </div> `})export class UserDetailComponent { http = inject(HttpClient);
userId = signal(1);
// Resource with reactive request user = rxResource({ request: () => ({ id: this.userId() }), loader: ({ request }) => this.http.get<User>(`/api/users/${request.id}`) }); // Automatiqueally reloads when userId() changements !
loadUser() { this.userId.update(id => id + 1); // user resource automatiqueally reloads! }}How it works:
requestreturns an object with dependencies- When any dependency changements,
loaderruns again - Loading states update automatiqueally
- Ancien requests sont cancelled automatiqueally
@Component({ selector: 'app-product-search', standalone: true, template: ` <div> <input [(ngModel)]="searchTerm" placeholder="Search" /> <select [(ngModel)]="category"> <option value="all">All</option> <option value="electronics">Electronics</option> <option value="books">Books</option> </select>
<div *ngIf="products.isLoading()">Searching...</div>
<div *ngFor="let product of products.value()"> {{ product.name }} - {{ product.category }} </div> </div> `})export class ProductSearchComponent { http = inject(HttpClient);
searchTerm = signal(''); category = signal('all');
// Automatiqueally reloads when searchTerm OR category changements products = rxResource({ request: () => ({ term: this.searchTerm(), cat: this.category() }), loader: ({ request }) => this.http.get<Product[]>('/api/products', { params: { search: request.term, category: request.cat } }) });}Result: Products automatiqueally reload when user types or changements category!
Loading Status
Section intitulée « Loading Status »@Component({ selector: 'app-data-view', standalone: true, template: ` <div [ngSwitch]="data.status()"> <div *ngSwitchCase="'idle'"> <button (click)="data.reload()">Load Data</button> </div>
<div *ngSwitchCase="'loading'"> <spinner></spinner> Loading... </div>
<div *ngSwitchCase="'error'"> <p class="error">{{ data.error()?.message }}</p> <button (click)="data.reload()">Retry</button> </div>
<div *ngSwitchCase="'success'"> <pre>{{ data.value() | json }}</pre> <button (click)="data.reload()">Refresh</button> </div> </div> `})export class DataViewComponent { http = inject(HttpClient);
data = rxResource({ loader: () => this.http.get('/api/data') });
// data.status() can be: // - 'idle' - Not loaded yet // - 'loading' - Currently loading // - 'error' - Failed à load // - 'success' - Successfully loaded}@Component({ template: ` <!-- Boolean helpers --> <div *ngIf="users.isLoading()">Loading...</div> <div *ngIf="users.hasValue()">Found {{ users.value()!.length }} users</div>
<!-- Value with default --> <div *ngFor="let user of users.value() ?? []"> {{ user.name }} </div>
<!-- Check error --> <div *ngIf="users.error()"> {{ users.error()!.message }} </div> `})export class UsersComponent { users = rxResource({ loader: () => this.http.get<User[]>('/api/users') });
// Available properties: // users.value() - Data or undefined // users.isLoading() - true/false // users.error() - Error or null // users.status() - 'idle' | 'loading' | 'error' | 'success' // users.hasValue() - true if value exists // users.reload() - Function à reload}Manual Reload
Section intitulée « Manual Reload »@Component({ selector: 'app-messages', standalone: true, template: ` <div> <button (click)="messages.reload()">Refresh Messages</button>
<div *ngFor="let message of messages.value()"> {{ message.text }} </div>
<p>Last updated: {{ lastUpdate }}</p> </div> `})export class MessagesComponent { http = inject(HttpClient); lastUpdate = signal(new Date());
messages = rxResource({ loader: () => { this.lastUpdate.set(new Date()); return this.http.get<Message[]>('/api/messages'); } });
// Call reload() à manually trigger a reload // Utile for refresh boutons, polling, etc.}@Component({ selector: 'app-live-data', standalone: true, template: ` <div> <h2>Live Stats</h2> <div *ngIf="stats.value()"> Users online: {{ stats.value()!.onlineUsers }} </div> </div> `})export class LiveDataComponent { http = inject(HttpClient);
stats = rxResource({ loader: () => this.http.get<Stats>('/api/stats') });
constructor() { // Poll every 5 seconds setInterval(() => { this.stats.reload(); }, 5000); }}Error Handling
Section intitulée « Error Handling »@Component({ selector: 'app-error-handling', standalone: true, template: ` <div> <!-- Show error message --> <div *ngIf="data.error()" class="alert-error"> <h3>Failed to load data</h3> <p>{{ data.error()!.message }}</p> <button (click)="data.reload()">Try Again</button> </div>
<!-- Show success --> <div *ngIf="data.value()"> <pre>{{ data.value() | json }}</pre> </div>
<!-- Error status --> <div *ngIf="data.status() === 'error'"> <p>Status Code: {{ getStatusCode() }}</p> </div> </div> `})export class ErrorHandlingComponent { http = inject(HttpClient);
data = rxResource({ loader: () => this.http.get('/api/data') });
getStatusCode() { const error = this.data.error() as any; return error?.status ?? 'Unknown'; }}@Component({ selector: 'app-custom-errors', standalone: true, template: ` <div> <div *ngIf="users.error()"> {{ getErrorMessage() }} <button (click)="users.reload()">Retry</button> </div>
<div *ngFor="let user of users.value()"> {{ user.name }} </div> </div> `})export class CustomErrorsComponent { http = inject(HttpClient);
users = rxResource({ loader: () => this.http.get<User[]>('/api/users').pipe( catchError(err => { // Transform error pour de meilleurs messages if (err.status === 404) { throw new Error('Users not found'); } else if (err.status === 403) { throw new Error('Access denied'); } else { throw new Error('Failed to load users'); } }) ) });
getErrorMessage(): string { return this.users.error()?.message ?? 'Unknown error'; }}Real-World Exemple: Pagination
Section intitulée « Real-World Exemple: Pagination »@Component({ selector: 'app-user-list-paginated', standalone: true, template: ` <div> <h2>Users (Page {{ currentPage() }})</h2>
<div *ngIf="users.isLoading()">Loading page...</div>
<div *ngIf="users.value()"> <div *ngFor="let user of users.value()!.data"> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> </div>
<div class="pagination"> <button (click)="prevPage()" [disabled]="currentPage() === 1 || users.isLoading()"> Previous </button>
<span>Page {{ currentPage() }} of {{ users.value()?.totalPages }}</span>
<button (click)="nextPage()" [disabled]="currentPage() === users.value()?.totalPages || users.isLoading()"> Next </button> </div> </div> </div> `})export class UserListPaginatedComponent { http = inject(HttpClient);
currentPage = signal(1); pageSize = 10;
// Automatiqueally reloads when currentPage changements users = rxResource({ request: () => ({ page: this.currentPage() }), loader: ({ request }) => this.http.get<PaginatedResponse<User>>('/api/users', { params: { page: request.page.toString(), pageSize: this.pageSize.toString() } }) });
nextPage() { this.currentPage.update(p => p + 1); }
prevPage() { this.currentPage.update(p => Math.max(1, p - 1)); }}
interface PaginatedResponse<T> { data: T[]; page: number; totalPages: number; total: number;}import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
@Component({ selector: 'app-product-search', standalone: true, template: ` <div> <input [(ngModel)]="searchTerm" placeholder="Search products..." (input)="onSearch()" />
<div *ngIf="products.isLoading()">Searching...</div>
<div *ngIf="products.value()"> Found {{ products.value()!.length }} products <div *ngFor="let product of products.value()"> {{ product.name }} - {{ product.price | currency }} </div> </div>
<div *ngIf="products.error()"> Search failed: {{ products.error()?.message }} </div> </div> `})export class ProductSearchComponent { http = inject(HttpClient);
searchTerm = signal('');
products = rxResource({ request: () => ({ term: this.searchTerm() }), loader: ({ request }) => { // Debounce inside loader return of(request.term).pipe( debounceTime(300), distinctUntilChanged(), switchMap(term => this.http.get<Product[]>('/api/products', { params: { search: term } }) ) ); } });
onSearch() { // Just update the signal, resource handles the rest this.searchTerm.set((event.target as HTMLInputElement).value); }}Bonnes Pratiques
Section intitulée « Bonnes Pratiques »- ✅ Utiliser pour loading data from APIs - Simplifies HTTP calls
- ✅ Perfect for lists, details, searches - Reactive data loading
- ✅ Great for pagination, filtering - Auau-reload on param changements
- ✅ Utiliser
requestfor reactive dependencies - Auau-reload when they change - ✅ Handle errors in template - Show user-friendly messages
- ✅ Utiliser
reload()for manual refresh - Refresh boutons, polling - ❌ Don’t use for POST/PUT/DELETE - Utiliser regular HttpClient
- ❌ Not for complex state - Consider a state management library
- ❌ Avoid nested resources - Can cause performance issues
rxResource vs Alternatives
Section intitulée « rxResource vs Alternatives »| Approach | Utiliser Case |
|---|---|
| rxResource | Loading data with automatique states |
| HttpClient | One-off requests, mutations (POST/PUT/DELETE) |
| Service + BehaviorSubject | Complex shsontd state across components |
| State management (NgRx) | Large-scale application state |
Common Patterns
Section intitulée « Common Patterns »| Pattern | Utiliser Case |
|---|---|
| Simple list | Load users, products, etc. |
| Detail view | Load single item by ID |
| Search | Reactive search with auau-reload |
| Pagination | Page through results |
| Filtering | Filter lists by criteria |
| Polling | Auau-refresh data periodically |
| Dependent requests | Load data based on another signal |

