Everything you need to know about the resource API in Angular ⚡️
In v19, Angular will introduce a new API for loading resources. This would allow us to fetch data from an API, know about the status of the request, and update the data locally when needed.
Intro
The resource API is straightforward in its core. Let’s see the most simple example of how to use it.
import { resource } from "@angular/core";
@Component({})
export class MyComponent {
todoResource = resource({
loader: () => {
return Promise.resolve({ id: 1, title: "Hello World", completed: false });
},
});
constructor() {
effect(() => {
console.log("Value: ", this.todoResource.value());
console.log("Status: ", this.todoResource.status());
console.log("Error: ", this.todoResource.error());
})
}
}
First, we can notice that the resource API uses Promises by default for the loader parameter. The other one is that the resource API will return an WritableResource
object, which helps us to update the data locally when needed.
We can read the current value of the resource by using the value
signal, the status of the resource by using the status
signal, and the error of the resource by using the error
signal.
The code above will print the following:
Value: undefined
Status: 'loading'
Error: undefined
Value: { id: 1, title: "Hello World", completed: false }
Status: 'resolved'
Error: undefined
Updating the data locally
Let’s see how we can update the data locally.
import { resource } from "@angular/core";
@Component({
template: `
<button (click)="updateTodo()">Update</button>
`
})
export class MyComponent {
todoResource = resource({
loader: () => {
return Promise.resolve({ id: 1, title: "Hello World", completed: false });
},
});
updateTodo() {
this.todoResource.value.update((value) => {
if (!value) return undefined;
return { ...value, title: "updated" };
});
}
}
We can update the data locally by using the update
method of the value
signal.
This will print the following:
Value: { id: 1, title: "updated", completed: false }
Status: 'local'
Error: undefined
The ‘local’ status means that the data was updated locally.
Loading the data
Let’s make a proper request to the server. Let’s load some todos from the JSONPlaceholder API.
interface Todo {
id: number;
title: string;
completed: boolean;
}
@Component()
export class MyComponent {
todosResource = resource({
loader: () => {
return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=10`)
.then((res) => res.json() as Promise<Todo[]>);
},
});
}
This todosResource
will start to make the request to the server immediately after it has been created.
Of course, the todosResource
will not have a value yet, because the request is still in progress.
The code above will print the following:
Value: undefined
Status: 'loading'
Error: undefined
Value: [{ id: 1, title: "Hello World", completed: false }, { id: 2, title: "Hello World", completed: false }, ...]
Status: 'resolved'
Error: undefined
Refreshing the data
Let’s say we want to refresh the data when the user clicks on a button.
import { resource } from "@angular/core";
@Component()
export class MyComponent {
todosResource = resource({
loader: () => {
return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=10`)
.then((res) => res.json() as Promise<Todo[]>);
},
});
refresh() {
this.todosResource.refresh();
}
}
The refresh
function will run the loader function again. If you call the refresh
function multiple times, the loader function will be called only once until the previous request is finished (like exhaustMap behavior in RxJS).
Loading specific data based on other signals
Let’s say we want to load the todos based on a todoId
signal.
import { resource } from "@angular/core";
@Component()
export class MyComponent {
todoId = signal(1);
todoResource = resource({
loader: () => {
return fetch(
`https://jsonplaceholder.typicode.com/todos/${this.todoId()}`
).then((res) => res.json() as Promise<Todo>);
},
});
}
This will work fine, but one thing to notice is that loader
is untracked
and that means, that if the todoId
signal changes, the load won't be called again. Let's make it more reactive!
Separate the request and the loader
We want our resource to refresh the data (call the loader again) every time the todoId
changes. For this, we can use the request
field of the resource. We can pass a signal to it, and it will be tracked.
todoResource = resource({
request: this.todoId,
loader: ({ request: todoId }) => {
return fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`,
).then((res) => res.json() as Promise<Todo>);
},
});
Now, when the todoId
signal changes, the resource API will automatically fetch the new data.
What if we have previous unfinished requests? Let’s say we want to cancel the previous request when the todoId
changes. Well, we can do that by using the abortSignal
that is passed to the loader function.
todoResource = resource({
request: this.todoId,
loader: ({ request: todoId, abortSignal }) => {
return fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`,
{ signal: abortSignal }
).then((res) => res.json() as Promise<Todo>);
},
});
This will cancel the previous request when the todoId
changes and if the previous request is still in progress.
We can also have multiple request dependencies, for example:
limit = signal(10);
query = signal('');
todosResource = resource({
request: () => ({ limit: this.limit(), query: this.query() }),
loader: ({ request, abortSignal }) => {
const { limit, query } = request as { limit: number; query: string };
return fetch(
`https://jsonplaceholder.typicode.com/todos?_limit=${limit}&query=${query}`,
{ signal: abortSignal }
).then((res) => res.json() as Promise<Todo[]>);
},
});
Now, the todosResource
will make the request based on the limit
and query
signals, and the loader function will be able to use those signals to make the request, and anytime the limit
or query
signal changes, the loader
function will be called again.
What happens when we have a request in progress and update data locally?
If that’s the case, the resource API will automatically update the data locally, but cancel the ongoing request.
How to postpone making the API request?
There are cases, when we don’t want to make the API request immediately, but do it only on some conditions only. A feature of the loader
is that it won’t make the API request if the request
returns undefined
. We can use that as the example below:
query = signal('');
pageIsActive = signal(true);
todosResource = resource({
request: () => pageIsActive() ? this.query(): undefined,
loader: ({ request: query, abortSignal }) => {
return fetch(
`https://jsonplaceholder.typicode.com/todos?query=${query}`,
{ signal: abortSignal }
).then((res) => res.json() as Promise<Todo[]>);
},
});
This enables us to only make the API request when the pageIsActive()
signal is true
. This may be the case when you use the resource
function in a service.
Create more reusable resources
By separating reactive values from the loader function, we can move the logic of the loader
function to a separate function, and store it where we want.
Before:
todoResource = resource({
request: this.todoId,
loader: ({ request: todoId, abortSignal }) => {
return fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`,
{ signal: abortSignal }
).then((res) => res.json() as Promise<Todo>);
},
});
After:
import { ResourceLoaderParams } from "@angular/core";
function todoLoader({ request: todoId, abortSignal }: ResourceLoaderParams<number>): Promise<Todo> {
return fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`,
{ signal: abortSignal }
).then((res) => res.json() as Promise<Todo>);
}
todoResource = resource({ request: this.todoId, loader: todoLoader });
The todoLoader
can be moved anywhere, and also can be reused by other resources. The ResourceLoaderParams
type is a type that contains all the information that is needed to make the request, and the request
parameter is the one that is passed to the loader function.
RxResource -> The observable-based resource API
Angular has always been about using Observables when it comes to data loading. This means, that we can use Observables to derive the data loading instead of using signals & promises.
To make this possible, we can use the rxResource
function.
import { rxResource } from "@angular/core/rxjs-interop";
@Component()
export class MyComponent {
limit = signal(10);
todosResource = rxResource({
request: this.limit,
loader: (limit) => {
return this.http.get<Todo[]>(
`https://jsonplaceholder.typicode.com/todos?_limit=${limit}`
);
},
});
}
This will make the request based on the limit signal, and the loader
function will be able to use the limit
value to make the request, and same as signals, anytime the limit signal changes, the loader function will be called again and cancel the previous request (same as switchMap behavior in RxJs)
And same as we can change the local state with the signals, we can also change the local state with the observable implementation in the rxResource
function.
Summary
We have 2 new primitives [resource, rxResource] that will help us make our life easier when dealing with data loading in Angular. These primitives have been requested for so long now, and will land in v19 as developer previews.
PR link: https://github.com/angular/angular/pull/58255
📹 Detailed explanation from Josh Morony
📢 Announcements
NgGlühwein 2024
For the first time ever the Push-Based team will host a workshop day on the day before the NgGlühwein conference. There will be two workshops held simultaneously:
Join conference for free (in-person & online) 👇:
https://www.meetup.com/angular-vienna/events/304004182
Thanks for reading!
If this article was interesting and useful to you, and you want to learn more about Angular, support me by buying me a coffee ☕️ or follow me on X (formerly Twitter) @Enea_Jahollari where I tweet and blog a lot about Angular
latest news, signals, videos, podcasts, updates, RFCs, pull requests and so much more. 💎