Angular 19: resource() API for async dependencies
Angular 19 introduces a powerful new reactive primitive called resource() that helps manage asynchronous dependencies through the signal system. Resources are particularly useful for handling API call
Core Concepts
A Resource consists of:
A reactive request function that determines what to fetch
An async loader function that handles the actual fetching
Signals exposing the current state and value
Built-in request cancellation and cleanup
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 same request.Resolved
: successfully loaded.Error
: loading failed.Local
: value was set manually.
Original vs Resource-based Implementation
I have a cats facts app build 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 interface 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',
standalone: true,
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 little card im 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 it will go from 2 (Loading) to 4 (Resolved) in our case.
Load more button (update locally)
You can upadte you resource locally this way:
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 values.
Restarting our facts (refreshing)
To be able to restart our request we just have to invoke the reload
method:
restartFacts(): void {
this.factsResource.reload();
}
And the status will 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
How about if you want to load more facts based on some signal? Let’s look at this:
@Injectable({ providedIn: 'root' })
export class CatsFactsService {
private readonly _apiUrl = 'https://meowfacts.herokuapp.com';
private readonly count = signal(10);
readonly getCatsFacts = resource({
loader: async () => {
try {
const response = await (await fetch(`${this._apiUrl}/?count=${this.count()}`)).json() as { data: string[] };
return response.data;
} catch(error) {
throw error;
}
}
});
updateCount(value: number): void {
this.count.set(value);
}
}
As you can see in the loader function, we added the count signal. But this won’t trigger the request again if we change the count. This is because the loder function is untracked
.
So, to be able to call again the request, we need to use the request
parameter from the resource object:
readonly getCatsFacts = resource({
request: this.count,
loader: async ({ request: count }) => {
try {
const response = await (await fetch(`${this._apiUrl}/?count=${count}`)).json() as { data: string[] };
return response.data;
} catch(error) {
throw error;
}
}
});
Now everytime we change our signal, the request will be triggered again!
AbortSignal
In the previous example we load the request again and again based on some signal change. But what if we change multiple times the signal, won’t be nice to cancel previous requests? We can do that by passing the abortSignal
to the loader function:
readonly getCatsFacts = resource({
request: this.count,
loader: async ({ request: count, abortSignal }) => {
try {
const response = await (await fetch(`${this._apiUrl}/?count=${count}`, { signal: abortSignal })).json() as { data: string[] };
return response.data;
} catch(error) {
throw error;
}
}
});
With this, if the previous request is still loading and we have a new one, the previous one will be canceled.
RxResource
The Angular team also introduced an Observable approach. So you can use Observables in your request. So, the loader function will return an Observable instead of a Promise. Let’s see it in action by refactoring our service:
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 interface 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({
request: this.count,
loader: (count) => {
return this._http
.get<CatFactResponse>(`${this._apiUrl}`, {
params: { count: 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 local state.
Conclusions
It’s a powerful API that need more feedback from the Angular community. Actually it’s experimental so try to play with it and make your own shapes. This adds more reactivity to our requests.
And as Alex from the Angular team said, there will be a Resource RFC.
This feature will be landed as experimental in Angular 19.
Important resources
Enea Jahollari was the first person to make a post about this feature and it’s really a greate article.
Our dessert friend (Manfred Steyer) also wrote a more techincal 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 like to have your feedback so please leave a comment, clap or follow. 👏
Spread the Angular love! 💜
If you really liked it, share it among your community, tech bros and whoever you want! 🚀👥
Don't forget to follow me and stay updated: 📱
Thanks for being part of this Angular journey! 👋😁