Angular's resource() API: Asynchronous Data Fetching with Signals
How Angular 20's stabilized resource() API fetches asynchronous data reactively with signals: core setup, configuration, resource state, route-driven loading, chaining resources, and the key considerations to keep in mind.
With the release of Angular 20, the Angular team stabilized the resource() API, a novel approach to asynchronously fetch data. The resource() API offers a powerful and reactive way to handle asynchronous data fetching in Angular, leveraging the capabilities of signals for a more declarative and integrated experience. It aims to seamlessly integrate with the popular signals library, participate in the signal graph, and provide built-in handling for loading states, error states, and refetching logic.
Traditional Asynchronous Data Fetching vs. resource()
Previously, handling asynchronous API requests, for instance, when a user clicks an item in a list to fetch detailed data, often involved several steps:
- A handler function (e.g.,
handleUserClick(userId)) would be called. - This function would typically invoke a domain service (e.g.,
UserService). - The service, with
HttpClientinjected, would expose a method (e.g.,getUserById(userId: string): Observable<User>) to make the backend request and return an Observable.
The resource() API offers a more streamlined and signal-integrated way to manage these scenarios.
Understanding resource()
Let’s explore how to load user data using resource().
Core Setup
The process involves two main parts:
- Creating a Request Signal: A signal is created to track the data that needs to be loaded. For example, to track the ID of a user to be fetched:
requestUserById = signal<string>("");
- Setting up the Resource: A
resourceis configured to monitor changes in the request signal and trigger the data loading process.
userResource = resource<User, { userId: string }>({
params: () => ({ userId: this.requestUserById() }),
loader: (payload: ResourceLoaderParams<{ userId: string }>) =>
firstValueFrom(this.userService.getUserById(payload.params.userId)),
});
Breakdown of resource() Configuration
The resource() method accepts an object with two key properties:
params:
- This function is responsible for creating a request object using signals.
- Any signals accessed within this function are tracked. When any of these tracked signals change, a new request object is generated, triggering the
loader. This behavior is similar tocomputed()signals, which re-evaluate when their dependent signals change. - In the example, it creates an object
{ userId: this.requestUserById() }. WheneverrequestUserById()signal changes, a new request is formed.
loader:
- This function performs the actual asynchronous task (e.g., an HTTP request) and returns a
PromiseLikeresponse. - It receives a
ResourceLoaderParamsobject as an argument, which can be de-structured to access its properties:- params: This holds the request object constructed by the
paramsfunction. - abortSignal: This
AbortSignalcan be used to cancel in-flight requests if a new request is queued before the current one completes. This is akin to theswitchMap()operator in RxJS. To utilize this, pass theabortSignalto APIs that support it. (Note: Angular’sHttpClientdoes not directly expose options forabortSignal). - previous: This property contains an object with a single
statusproperty, which holds the previous status of the resource.
- params: This holds the request object constructed by the
Triggering and Consuming the Resource
1. Triggering a Request: A resource setup at class level with a valid request initiates a fetch when the class is created. However, to make additional requests, simply update the signal that the params function tracks. For instance, in a handleUserClick(userId) method:
requestUserById.set(userId);
When requestUserById is updated, the resource automatically:
- Detects the change in the tracked signal.
- The
requestfunction curates a new request object. - The
loaderreceives this new request and dispatches the asynchronous operation (e.g., fetching user data).
2. Consuming the Response and Resource State: The userResource variable itself is a signal and provides several properties and methods to manage and observe the resource’s state:
- hasValue(): boolean - Returns
trueif the resource has successfully fetched a value,falseotherwise. - destroy(): void - Destroys the resource, cancels any pending requests, and resets its state to
Idle. - value: Signal<User | undefined> - A signal holding the successfully fetched value from the
loader. It’sundefinedif no value has been resolved or an error occurred. - isLoading: Signal<boolean> - A signal that returns
trueif theloaderis currently executing an asynchronous request,falseotherwise. - reload(): void - Re-triggers the resource with the current request object, forcing a fresh data fetch.
- error: Signal<unknown | undefined> - A signal that holds the last known error if the most recent fetch call failed. It’s
undefinedotherwise. - status: Signal<ResourceStatus> - A signal representing the current status of the resource. The possible
ResourceStatusenum values are:- Idle (0) - The resource has no valid request and will not perform requests.
- Error (1) - The most recent fetch call failed.
- Loading (2) - A fetch call is pending (same as
isLoadingbeingtrue). - Reloading (3) - Similar to
Loading, but specifically when a resource is reloaded for an unchanged request. - Resolved (4) - The request has completed successfully, and a value is available.
- Local (5) - The resource’s value has been manually set or updated using
set()orupdate()methods. AResourceRef(returned byresource()) extendsWritableSignal, making these methods available.update()receives the last loaded value and expects a new value of the same type.set()expects a value of the same type resolved by theloader.
Practical Example: Initializing with Route Parameters and Handling Loading States
Let’s enhance the example to initialize the requestUserById signal from URL parameters and manage loading indicators.
1. Initializing requestUserById from Route Parameters: Assuming a route like /customers/:customerId, you can initialize the signal using ActivatedRoute:
private activatedRoute = inject(ActivatedRoute);
private userService = inject(UserService);
private injector = inject(Injector);
private destroyRef = inject(DestroyRef);
requestUserById = signal<string | null>(
this.activatedRoute.snapshot.paramMap.get('customerId')
);
userResource = resource<User | undefined, { userId: string | null }>({
params: () => ({ userId: this.requestUserById() }),
loader: (payload: ResourceLoaderParams<{ userId: string | null }>) => {
if (payload.request.userId) {
return firstValueFrom(this.userService.getUserById(payload.params.userId));
}
return Promise.reject('User ID not provided');
}
});
Upon component initialization, if customerId is present in the URL, userResource will automatically dispatch a fetch request.
2. Displaying a Loading Indicator: You can use the isLoading signal from the resource to show/hide a loading component (e.g., app-loading). Furthermore, you can utilize the hasValue and error signals from the resource to conditionally show value of the resource or any errors.
<app-loading [loading]="userResource.isLoading()"></app-loading>
@if (userResource.hasValue()) {
<pre>{{ userResource.value() | json }}</pre>
}
@if (userResource.error()) {
<p>Error loading user: {{ userResource.error() | json }}</p>
}
3. Handling Loading Events Programmatically: If you need to perform actions based on the loading state (e.g., showing/hiding a global loader service), you can convert the isLoading signal from the resource to an Observable by using toObservable inter-op function.
ngOnInit() {
this.handleLoadingEvent();
}
handleLoadingEvent() {
toObservable(this.userResource.isLoading, { injector: this.injector })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((isLoading) => {
if (isLoading) {
this.loader.show();
} else {
this.loader.hide();
}
});
}
Note: toObservable() requires an injection context. If used outside of one (e.g., in a service method not called during construction or in a non-DI context), you must provide an Injector instance. The handleLoadingEvent should typically be called within a lifecycle hook like ngOnInit.
4. Using linkedSignal for More Control Over Loading State: For scenarios where other parts of your page might also contribute to an overall loading state, you can use linkedSignal. A linkedSignal is a writable computed signal. You can initialize it based on other signals (like userResource.isLoading()) and also set/update its value directly. Setting a value to the linkedSignal does not override the computed setup it was initialized with.
pageLoading = linkedSignal(() => this.userResource.isLoading());
Later in your component, you can manually set this signal if needed. For example, if another async operation starts:
this.pageLoading.set(true);
You would then use pageLoading() in your template or other logic.
Reacting to Another Resource’s Response
Consider a scenario where one resource needs to act based on the response of another. In this example, let’s say the User object returned from the backend includes a profilePictureID. We’ll use this ID to request the actual image data.
We can set up a new resource, profilePictureResource, that depends on the userResource. The profilePictureResource will use the profilePictureID from the userResource’s value to call an imageService and load the image.
profilePictureResource = resource({
params: () => this.userResource.value(),
// Uses the loaded user data
loader: ({ params }) =>
// 'params' here is the userResource.value()
this.imageService.getImageById(params.profilePictureID)
// Fetches the image
});
This class-level setup for profilePictureResource only executes after userResource successfully retrieves its data from the backend. Once userResource has a value, profilePictureResource automatically triggers its loader function, calling imageService.getImageById with the appropriate ID.
The stateful signal properties of profilePictureResource can then be used in your UI to display a loading indicator while the image is being fetched. Or better yet, update your pageLoading linkedSignal property to track this resource as well.
pageLoading = linkedSignal(
() => this.userResource.isLoading() || this.profilePictureResource.isLoading()
);
Important Considerations
- Injection Context:
resource()is ideally set up at a class level (e.g., component properties) where it’s inherently within an injection context. If created outside an injection context (e.g., within a method called later), anInjectorinstance must be provided as an argument alongsideparamsandloader. - Use Case: If you attempt to send a mutation (POST, PUT, DELETE) through the
resource’sloaderfunction, and then theparamsfor thatresourcechange (triggering an abort of the previous loader execution), the mutation might be cancelled mid-flight. This means the server never receives or processes the intended change. Your UI, expecting the mutation to succeed, might optimistically update, while the database remains unchanged. This is why the Angular docs recommend usingresource()primarily for data fetching rather than data modification operations.
A Stackblitz representation of most of the concepts we have covered here along with an equivalent way of doing the same setup in RXJS world.