Home >Web Front-end >JS Tutorial >Angular resource() and rxResource() APIs: what you need to know
The release of Angular v19, just a few weeks ago, marks a significant milestone in the signal revolution within the framework, with the Input, Model, Output and Signal Queries APIs now officially promoted to stable.
But that's not all! This major version also introduces powerful new tools designed to further advance the signal revolution: the new Resource API.
As the name suggests, this new Resource API is designed to simplify loading asynchronous resources by harnessing the full power of signals!
IMPORTANT: at the time of writing, the new Resource API is still experimental. This means it may change before becoming stable, so use it at your own risk. ?
Let's dive into how it works and how it simplifies handling async resources!
Most signal APIs are synchronous, but in real-world applications, it is essential to handle asynchronous resources, such as fetching data from a server or managing user interactions in real-time.
This is where the new Resource API comes into play.
Using a Resource, you can easily consume an asynchronous resource via signals, allowing you to easily manage data fetching, handle loading states, and trigger a new fetch whenever the associated signal parameters change.
The easier way to create a Resource is by using the resource() function:
import { resource, signal } from '@angular/core'; const RESOURCE_URL = 'https://jsonplaceholder.typicode.com/todos/'; private id = signal(1); private myResource = resource({ request: () => ({ id: this.id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id), });
This function accepts a ResourceOptions configuration object as input, allowing you to specify the following properties:
Thanks to these configurations, we can easily define an asynchronous dependency that will always be consumed efficiently and kept up-to-date.
Once a Resource is created, the loader function is executed, then the resulting asynchronous request starts:
import { resource, signal } from '@angular/core'; const RESOURCE_URL = 'https://jsonplaceholder.typicode.com/todos/'; private id = signal(1); private myResource = resource({ request: () => ({ id: this.id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id), });
Whenever a signal that the request function depends on changes, the request function runs again, and if it returns new parameters, the loader function is triggered to fetch the updated resource's value:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading")
If no request function is provided, the loader function will run only once, unless the Resource is reloaded using the reload method (more below).
Finally, once the parent component or service is destroyed, the Resource is also destroyed unless a specific injector has been provided.
In such cases, the Resource will remain active and be destroyed only when the provided injector itself is destroyed.
To optimize data fetching, a Resource can abort an outstanding requests if the request() computation changes while a previous value is still loading.
To manage this, the loader() function provides an abortSignal, which you can pass to ongoing requests, such as fetch. The request listens for the abortSignal and cancels the operation if it's triggered, ensuring efficient resource management and preventing unnecessary network requests:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 1 , ... } id.set(2); // Triggers a request, causing the loader function to run again console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 2 , ... }
Based on this, it's recommended to use the Resource API primarily for GET requests, as they are typically safe to cancel without causing issues.
For POST or UPDATE requests, canceling might lead to unintended side effects, such as incomplete data submissions or updates. However, if you need similar functionality for these types of requests, you can use the effect() method to safely manage the operations.
The Resource API provides several signal properties for its state, that you can easily use directly within your components or services:
Here's an example of how to consume a Resource within a component:
import { resource, signal } from '@angular/core'; const RESOURCE_URL = 'https://jsonplaceholder.typicode.com/todos/'; private id = signal(1); private myResource = resource({ request: () => ({ id: this.id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id), });
In this example, the Resource is used to fetch data from an API based on the value of the id signal, which can be incremented by clicking a button.
Whenever the user clicks the button, the id signal value changes, triggering the loader function to fetch a new item from the remote API.
The UI automatically updates with the fetched data thanks to the signals properties exposed by the Resource API.
As mentioned earlier, the status signal provides information about the current state of the resource at any given moment.
The possible values of the status signal are defined by the ResourceStatus enum. Here's a summary of these statuses and their corresponding values:
These statuses help track the Resource's progress and facilitate better handling of asynchronous operations in your application.
Given the complexity of these statuses, the Resource API provides a hasValue() method, which returns a boolean based on the current status.
This ensures accurate information about the Resource's status, providing a more reliable way to handle asynchronous operations without relying on the value, which might be undefined in certain states.
import { resource, signal } from '@angular/core'; const RESOURCE_URL = 'https://jsonplaceholder.typicode.com/todos/'; private id = signal(1); private myResource = resource({ request: () => ({ id: this.id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id), });
This method is reactive, allowing you to consume and track it like a signal.
The Resource API also provides an isLoading signal, which returns whether the resource is currently in the Loading or Reloading state:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading")
Since isLoading is a computed signal, it can be tracked reactively, allowing you to monitor the loading state in real-time using signals APIs.
The value signal provided by a Resource is a WritableSignal, which allows you to update it manually using the set() and update() functions:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 1 , ... } id.set(2); // Triggers a request, causing the loader function to run again console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 2 , ... }
Note: as you can see, manually updating the value of the signal will also set the status to 5, which means "Local", to indicate that the value was set locally.
The manually set value will persist until either a new value is set or a new request is performed, which will override it with a new value:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request, abortSignal }) => fetch(RESOURCE_URL + request.id, { signal: abortSignal }) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // Triggers a new request, causing the previous fetch to be aborted // Then the loader function to run again generating a new fetch request id.set(2); console.log(myResource.status()); // Prints: 2 (which means "Loading")
Note: the value signal of the Resource API uses the same pattern of the new LinkedSignal API, but does not use it under the hood. ?
To simplify the use of the value signal, the Resource API provides convenience wrappers for the set, update, and asReadonly methods.
The asReadonly method is particularly useful as it returns a read-only instance of the value signal, allowing access only for reading and preventing any accidental modifications.
You can use this approach to create services that manage and track changes to resource values by exporting a read-only instance of the value:
import { Component, resource, signal } from '@angular/core'; const BASE_URL = 'https://jsonplaceholder.typicode.com/todos/'; @Component({ selector: 'my-component', template: ` @if (myResource.value()) { {{ myResource.value().title }} } <button (click)="fetchNext()">Fetch next item</button> ` }) export class MyComponent { private id = signal(1); protected myResource = resource({ request: () => ({ id: this.id() }), loader: ({ request }) => fetch(BASE_URL + request.id).then((response) => response.json()), }); protected fetchNext(): void { this.id.update((id) => id + 1); } }
This will prevent consumers from modifying the value, reducing the risk of unintended changes, improving consistency in complex data management.
When working with asynchronous resources, you may face scenarios where refreshing the data or destroy the Resource becomes necessary.
To handle these scenarios, the Resource API provides two dedicated methods that offer efficient solutions for managing these actions.
The reload() method instructs the Resource to re-execute the asynchronous request, ensuring it fetches the most up-to-date data:
import { resource, signal } from '@angular/core'; const RESOURCE_URL = 'https://jsonplaceholder.typicode.com/todos/'; private id = signal(1); private myResource = resource({ request: () => ({ id: this.id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id), });
The reload() method returns true if a reload is successfully initiated.
If a reload cannot be performed, either because it is unnecessary, such as when the status is already Loading or Reloading, or unsupported, like when the status is Idle, the method returns false.
The destroy() method manually destroys the Resource, destroying any effect() used to track request changes, canceling any pending requests, and setting the status to Idle while resetting the value to undefined:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading")
After a Resource is destroyed, it will no longer respond to request changes or reload() operations.
Note: at this point, while the value signal remains writable, the Resource will lose its intended purpose and no longer serves its function, becoming useless. ?
Like nearly all signal-based APIs introduced so far, the Resource API also offers an interoperability utility for seamless integration with RxJS.
Instead of using the resource() method to create a Promise-based Resource, you can use the rxResource() method to use Observables:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 1 , ... } id.set(2); // Triggers a request, causing the loader function to run again console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 2 , ... }
Note: the rxResource() method is in fact exposed by the rxjs-interop package.
The Observable produced by the loader() function will consider only the first emitted value, ignoring subsequent emissions.
Thank you all for following me throughout this wonderful 2024. ??
It has been a year full of challenges, but also very rewarding. I have big plans for 2025 and I can't wait to start working on them. ?
I’d like to have your feedback so please leave a comment, like or follow. ?
Then, if you really liked it, share it among your community, tech bros and whoever you want. And don’t forget to follow me on LinkedIn. ??
The above is the detailed content of Angular resource() and rxResource() APIs: what you need to know. For more information, please follow other related articles on the PHP Chinese website!