A Technical Deep Dive into Angular's resource(), rxResource() and httpResource() APIs
Angular 19 introduces a suite of Resource APIs that help manage asynchronous dependencies through the signal system. This evolution positions Angular to deliver better Core Web Vitals.
Resource API
To create a resource
, a developer uses the resource()
function, which accepts a ResourceOptions
object with two main properties: params
and loader
or stream
.
The params
property is a reactive computation that generates a parameter value for the asynchronous operation. When any signals read within this computation change, a new parameter value is produced, and the resource automatically re-fetches the data by invoking the loader
function.
The loader
is an asynchronous function that performs the actual data retrieval, such as a fetch()
call or a Promise
-based operation.
Loader vs Stream
Angular 20 introduces a crucial distinction in resource()
between loader
and stream
that goes beyond just Promise vs Observable handling—it's about single-value vs multi-value data flows:
When to use loader
(Single-value operations):
// Use loader for one-time data fetching that returns a single result
const userResource = resource({
params: () => this.userId,
loader: async (loaderParams: ResourceLoaderParams<string>) => {
const response = await fetch(`/api/users/${loaderParams.params}`, {
signal: loaderParams.abortSignal
});
return response.json() as User; // Single value returned
}
});
When to use stream
(Multi-value streaming operations):
// Use stream for data that emits multiple values over time
const liveNotificationsResource = resource({
params: () => ({ userId: this.currentUserId }),
stream: async (loaderParams) => {
const userId = loaderParams.params.userId;
// 1. Create Signal representing the stream
const resultSignal = signal<ResourceStreamItem<Notification[]>>({
value: []
});
// 2. Set up WebSocket connection for live notifications
const ws = new WebSocket(`wss://api.example.com/notifications/${userId}`);
ws.onmessage = (event) => {
const newNotification = JSON.parse(event.data) as Notification;
const currentNotifications = resultSignal().value || [];
resultSignal.set({
value: [newNotification, ...currentNotifications]
});
};
ws.onerror = () => {
resultSignal.set({
error: new Error('Connection failed')
});
};
// 3. Set up cleanup when stream is no longer needed
loaderParams.abortSignal.addEventListener('abort', () => {
ws.close();
});
// 4. Return the streaming signal
return resultSignal;
}
});
Key Architectural Differences:
loader: Returns
Promise<T>
- designed for traditional async operations that resolve oncestream: Returns
PromiseLike<Signal<ResourceStreamItem<T>>>
- designed for continuous data emissionUse cases for loader: HTTP requests, file operations, single database queries
Use cases for stream: Real-time data, timers, WebSocket connections, live updates
Error handling: Streams can recover from errors and continue emitting values, while loader errors terminate the operation
Stream Semantics: Streaming resources use switch-map semantics—when params change, the old stream is cancelled and a new one begins. This prevents memory leaks and ensures only the most recent stream is active.
This architectural choice allows resource()
to handle both traditional async patterns and modern streaming requirements within a unified API, while maintaining the reactive benefits of Angular's signal system.
Resource States
Resources can be in one of these states (ResourceStatus enum):
Idle
: no valid request, no loading.Loading
: initial load for a request.Reloading
: loading fresh data for the same request.Resolved
: successfully loaded.Error
: loading failed.Local
: value was set manually.
Original vs Resource-based Implementation
I have a cat’s facts app built with Signals
and we are going to refactor the request to be able to use the resource() API
.
First, let's look at how we can refactor the CatsFactsService
:
// cats-facts.service.ts
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { catchError, map, Observable } from 'rxjs';
export type CatFactResponse {
data: string[];
}
@Injectable({ providedIn: 'root' })
export class CatsFactsService {
private readonly _apiUrl = 'https://meowfacts.herokuapp.com';
private readonly _http = inject(HttpClient);
// We have to refactor this
getCatsFacts(count = 10): Observable<string[]> {
return this._http
.get<CatFactResponse>(`${this._apiUrl}`, {
params: { count: count.toString() },
})
.pipe(
map((response) => response.data),
catchError(this._handleError)
);
}
private _handleError(error: any): Observable<never> {
console.error('An error occurred:', error);
throw error;
}
}
So, let’s refactor it using the new resource API:
readonly getCatsFacts = resource({
loader: async () => {
try {
const response = await (await fetch(`${this._apiUrl}/?count=10`)).json() as { data: string[] };
return response.data;
} catch(error) {
throw error;
}
}
});
As you can see the resource()
has a loader
parameter that returns a Promise
.
Consume resource()
In our controller we can consume it this way:
@Component({
selector: 'app-cat-facts',
templateUrl: './cats-facts.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CatFactsComponent {
private readonly _catFactsService = inject(CatsFactsService);
factsResource = this._catFactsService.getCatsFacts;
}
And in our template we can use the new @let
syntax to reference all the values from the resource:
@let facts = factsResource.value() || [];
@let hasValue = factsResource.hasValue();
@let status = factsResource.status();
@let isLoading = factsResource.isLoading();
@let error = factsResource.error();
Then you can loop the facts and print the values. In the picture you can see that with the little card I’m referencing those values:
I have a pipe
that resolve the resource()
status:
@Pipe({
name: 'resourceStatus',
standalone: true,
})
export class ResourceStatusPipe implements PipeTransform {
transform(status: ResourceStatus): string {
switch (status) {
case ResourceStatus.Idle:
return 'Idle';
case ResourceStatus.Error:
return 'Error';
case ResourceStatus.Loading:
return 'Loading';
case ResourceStatus.Resolved:
return 'Resolved';
case ResourceStatus.Reloading:
return 'Reloading';
case ResourceStatus.Local:
return 'Local';
default:
return 'Unknown';
}
}
}
If I print with a console.log
the status path will go from 2 (Loading) to 4 (Resolved).
Update Locally (Load more button)
You can update the resource
locally:
loadMore(): void {
this.factsResource.value.update((values: string[] | undefined) => {
if (!values) {
return undefined;
}
return [...values, 'Other fact!' ];
});
this.count.update((ct) => (ct += 5));
}
The update
method adds one more item to our list, and our state changes to 5 (ResourceStatus.Local
). Note that you can also use the set
method to replace the facts with a new value.
Restarting our facts (refreshing)
To be able to restart our request, we have to invoke the reload
method:
restartFacts(): void {
this.factsResource.reload();
}
The status will then change from 3 (Reloading) to 4 (Resolved).
Note that if you click multiple times on the Restart button, the loader function will only be called once, just as the switchMap
operator from RxJS works.
Load 10, 20, 30… facts
What if we want to load more facts dynamically based on a specific signal? In our previous example, we only worked with the loader
parameter, but this approach has a limitation: changing the count won't trigger a new request because the loader function is untracked
.
Since untracked functions don't react to signal changes, we need a different approach to make our data loading reactive. The solution is to use the request
parameter to pass the signal directly, enabling the loader to respond to state changes and automatically refetch data when needed.
@Injectable({ providedIn: 'root' })
export class CatsFactsService {
private readonly _apiUrl = 'https://meowfacts.herokuapp.com';
private readonly count = signal(10);
readonly getCatsFacts = resource({
params: this.count,
loader: async (loaderParams: ResourceLoaderParams<number>) => {
try {
const response = (await (
await fetch(`${this._apiUrl}/?count=${loaderParams.params}`, {
signal: loaderParams.abortSignal,
})
).json()) as { data: string[] };
return response.data;
} catch (error) {
throw error;
}
},
defaultValue: [],
});
updateCount(value: number): void {
this.count.set(value);
}
}
Every time we change our signal, the request will be triggered again!
AbortSignal
In the previous example, we reload the request whenever a signal changes. But what happens when the signal changes multiple times in quick succession? Without proper handling, we could end up with multiple overlapping requests, potentially causing race conditions or wasted resources.
Fortunately, we can solve this by canceling previous requests when new ones are initiated. This is achieved by passing an abortSignal
to the loader function, which automatically cancels any in-flight requests when a new one begins.
await fetch(`${this._apiUrl}/?count=${loaderParams.params}`, {
signal: loaderParams.abortSignal,
})
With this, if the previous request is still loading and we have a new one, the previous one will be cancelled.
RxResource
The rxResource() function bridges Observable-based data loading with Angular's resource pattern, designed specifically for applications leveraging RxJS operators and existing Observable workflows.
The stream
function returns an Observable that rxResource automatically subscribes to, with dependency tracking based on signal usage within the stream function.
import { HttpClient } from '@angular/common/http';
import { inject, Injectable, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
export type CatFactResponse = {
data: string[];
};
@Injectable({ providedIn: 'root' })
export class CatsFactsRxResourceService {
private readonly _http = inject(HttpClient);
private readonly _apiUrl = 'https://meowfacts.herokuapp.com';
private readonly _count = signal(10);
readonly getCatsFacts = rxResource({
stream: () => {
return this._http
.get<CatFactResponse>(`${this._apiUrl}`, {
params: { count: this._count().toString() },
})
.pipe(
map((response) => response.data),
catchError(this._handleError)
);
},
});
updateCount(value: number): void {
this._count.set(value);
}
private _handleError(error: any): Observable<never> {
console.error('An error occurred:', error);
throw error;
}
}
The same behaviour will happen, you can cancel previous requests if new ones are triggered and also change the local state.
HttpResource
The httpResource() function represents the most streamlined approach to HTTP GET operations, built on top of resource() and using Angular's HttpClient internally. It's specifically optimized for straightforward HTTP requests without requiring extensive Observable manipulation or Promise handling.
import { Component, input } from '@angular/core';
import { httpResource } from '@angular/common/http';
@Component({
selector: 'app-product-details',
template: `
@if (product.hasValue()) {
<div class="product">
<h1>{{ product.value().name }}</h1>
<p>Price: {{ product.value().price | currency }}</p>
<button (click)="product.reload()">Refresh</button>
</div>
} @else if (product.error()) {
<div class="error">Product not found</div>
} @else if (product.isLoading()) {
<div class="loading">Loading product...</div>
}
`
})
export class ProductDetailsComponent {
productId = input.required<string>();
product = httpResource<Product>(() => `/api/products/${this.productId()}`, {
defaultValue: null
});
}
Advanced HTTP configuration supports complex request scenarios through request object syntax, enabling custom headers, parameters, and HTTP options while maintaining the simplified resource interface:
@Component({})
export class AdvancedHttpComponent {
query = signal<string>('');
sortOrder = signal<'asc' | 'desc'>('asc');
products = httpResource(() => ({
url: '/api/products/search',
method: 'GET',
params: {
q: this.query(),
sort: this.sortOrder()
},
headers: { 'X-API-Version': '2.0' },
reportProgress: true
}), {
defaultValue: [],
parse: (data) => ProductArraySchema.parse(data) // Zod validation
});
}
Choosing Between resource() and httpResource() for HTTP Operations
While both resource()
and httpResource()
can handle HTTP requests, they serve different purposes and architectural needs. Understanding when to use each is crucial for optimal application design.
Use httpResource() when:
Making straightforward HTTP GET requests
You want automatic HttpClient integration (interceptors, testing, error handling)
You need minimal configuration and setup
You're building typical CRUD read operations
// httpResource - Optimized for simple HTTP operations
const userProfile = httpResource<User>(() => `/api/users/${this.userId()}`, {
defaultValue: null
});
// Automatic headers, interceptors, and error handling included
Use resource() when:
You need custom HTTP logic or non-standard request handling
You want to combine HTTP with other async operations
You need streaming data over HTTP (Server-Sent Events, custom protocols)
You require fine-grained control over the request lifecycle
// resource() - Full control over HTTP implementation
const userProfile = resource({
params: () => this.userId,
loader: async (loaderParams: ResourceLoaderParams<string>) => {
// Custom fetch logic with specific error handling
const response = await fetch(`/api/users/${loaderParams.params}`, {
signal: loaderParams.abortSignal,
headers: {
'Custom-Auth': await this.getAuthToken(),
'X-Request-ID': crypto.randomUUID()
}
});
if (!response.ok) {
// Custom error processing
const errorData = await response.json();
throw new CustomApiError(errorData.message, response.status);
}
return response.json() as User;
}
});
Practical Decision Framework:
Simple HTTP GET operations → Use
httpResource()
Need HttpClient interceptors → Use
httpResource()
Standard REST API consumption → Use
httpResource()
Custom authentication headers → Use
resource()
Non-HTTP async operations → Use
resource()
Streaming HTTP data → Use
resource()
with streamComplex error handling logic → Use
resource()
Conclusions
The Resource API is a powerful new feature that brings enhanced reactivity to our HTTP requests. Since it's currently experimental, the Angular team is actively seeking feedback from the community to help shape its future development.
This is your opportunity to explore this innovative API and experiment with different patterns and use cases. The more developers test and provide input, the better the final implementation will be for everyone.
Ready to contribute? Try out the Resource API in your projects and share your feedback and opinions in the Resource RFC. Your real-world experience and insights will help make this feature even more powerful and developer-friendly.
Important resources
Enea Jahollari was the first person to make a post about this feature, and it’s really a great article.
Our dessert friend (Manfred Steyer) also wrote a more technical article about it, and the best part for me is when he explains important things about abortSignal.
Tech Stack Nation with a video where Alex plays with Resource API.
Thanks for reading so far 🙏
I’d love to hear your thoughts, so please leave a comment, clap, or follow. 👏
Spread the Angular enthusiasm! 💜
If you enjoyed it, share it with your community, tech friends, and anyone else! 🚀👥
Don't forget to follow me and stay in the loop: 📱
Thanks for being part of this Angular adventure! 👋😁
🐦 Twitter
Thanks for being part of this Angular journey! 👋😁