rxResource (Angular 21)
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?
- ✅ 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
| 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
- Basic Example
- Without rxResource
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 automatique
users = rxResource({
loader: () => this.http.get<User[]>('/api/users')
});
// All states handled automatiqueally!
Reactive Loading with Request
- With Request Signal
- Multiple Dependencies
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
- Status Values
- Helper Properties
@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
- Reload Method
- Auau-Polling
@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
- Display Errors
- Personnalisé 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
- Paginated List
- Search with Debounce
@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
- ✅ 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
| 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
| 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 |