Resource: async loading with signals
Who said you needed an effect ?
Matthieu Riegler -
Defining APIs and patterns around them is a task that requires time, feedback loops and taking a step back.
As the Signal APIs mature in Angular (most of them are being promoted to stable in v19, exception of effect
), it
becomes time to have a look at more advanced patterns.
The usage of effect
has been a debated topic, Alex Rickabaugh, the framework lead of the Angular Team has been pretty vocal on recommending to use them as less as possible.
There are alternatives, often abstractions on top of if which express more the intention of the developers.
Introducing the resource
API.
The resource
API is one of those APIs that is intended to abstract the usage of effect
and provide a more declarative signal-based API
for asynchronous loading of data.
As a bare minimum resource
requires a loader
that returns async data and returns an WritableResource
wrapper object.
The loader
function is invoked eagerly (as part of an effect
).
import {resource} from '@angular/core';
@Component({
template: `
value: {{ myResource.value() }}
`,
})
export class MyComponent {
myResource: WritableResource = resource({
loader: () => {
return Promise.resolve('My data is loaded');
},
});
}
This example will display nothing (undefined
) until the data is loaded and My data is loaded
once the promise is resolved.
The loading status as a key information
When we are loading data asynchronously, it is important to keep track of our loading status. Is it loading ? Is it done ? Did I get an error ?
Famously, having a isLoading
boolean is considered an anti-pattern as it lacks a lot of information around it.
A WritableResource
holds 3 key information.
value
, a signal wrapped value for the loadedstatus
, a signal wrapped value of the loading status ('idle' | 'error' | 'loading' | 'refreshing' | 'resolved' | 'local'
)error
, a signal wrapped object for the details of the error, loading resulted in an error.
If we capture this in an effect
we would observe following:
Capturing resource changes
effect(() => {
console.log('value: ', this.myResource.value());
console.log('status: ', this.myResource.status(), myResource.status());
console.log('error: ', this.myResource.error());
});
Output
value: undefined
status: 0 Idle
error: undefined
value: 'My data is loaded'
status: 3 Resolved
error: undefined
Fetching data
You'll probably reach out to resource
for fetching async data over HTTP, but it could be any other async API that is promised based.
Let's take an example using fetch
.
Basic fetch resource example
fetch
returns a promise which is passed to resource
as the loader. The fetch
request emitted instantly (as part of an effect()
) without
any template or additional effect
consuming the WritableResource
.
postResource = resource({loader: () => fetch(`https://dummyjson.com/posts/1`)});
Reacting to signal changes
One of the missing building pieces has been about reacting to signal changes and reloading data as a consequence.
The resource
API addresses this requirement by declaring dependencies as part of a request
.
Listening to a signal change
The dependencies are declared as part of the request
callback.
postResource = resource({
request: this.postId, // this could be one of your signal inputs
loader: ({request: postId}) => return fetch(`https://dummyjson.com/posts/${postId}`),
});
Signals read as part of the loader callback will not trigger further loading upon changes
(The loader
's execution is untracked
).
If you're asking yourself why the loader function isn't tracked, it's because the async part of the loader function wouldn't be tracked.
So to make it more clear reactivity-wise, the reactive part (the request
) was separated for the loading part (the loader
).
If the loader was a reactive context
postResource = resource({
request: () => this.postId(),
loader: async ({request}) => {
// signals can be tracked
const response = await fetch(`https://dummyjson.com/posts/${request}`);
const result = await response.json();
// signals can't be tracked
},
});
Listening to multiple signal changes
A more realistic use case would be to listen to multiple signal at once and react to the change of each of them.
postResource = resource({
request: () => ({limit: this.limit(), filter: this.filter(), select: this.select()}),
loader: ({request: {limit, filter, select}}) => {
return fetch(`https://dummyjson.com/posts/search?q=${filter}&limit=${limit}&select=${select}`);
},
});
Skipping requests
The loader is conditionnaly firing depending on the request itself. If it returns undefined
, the loader won't fire.
This way you can skip some request (the status remains idle
until the first request fires).
postResource = resource({
request: () => (this.postId() > 5 ? this.postId() : undefined),
loader: ({request}) => {
return fetch(`https://dummyjson.com/posts/${request}`).then((res) => res.json());
},
});
Complete fetch resource example
More realisticaly, we'll parse the response as JSON and might want to plug-in the abortSignal
.
The abortSignal
will be used to cancel the pending request when one of the signals receives a new value.
postResource = resource({
request: () => ({limit: this.limit(), filter: this.filter(), select: this.select()}),
loader: ({request: {limit, filter, select}, abortSignal}) => {
return fetch(`https://dummyjson.com/posts/search?q=${filter}&limit=${limit}&select=${select}`, {
signal: abortSignal,
}).then((res) => res.json() as Promise<Post[]>);
},
});
Refreshing the data
In the case that your resource data gets invalidated on the server, imagine deleting a post for example, you'd like to refresh your local data
to get the latest state from the server. For this you'll reach out to the reload()
method.
If your resource isn't already loading (status() === 'loading'
), reload()
will set your resource status
to 'reloading'
, and send a new request.
In the meantime, your resource will keep its previous value until the new one is received.
import {resource} from '@angular/core';
@Component()
export class MyComponent {
postResource = resource({
loader: () => {
return fetch(`https://dummyjson.com/posts/search?limit=${limit}`).then(
(res) => res.json() as Promise<Todo[]>,
);
},
});
/* Invalidate local data & request the new state */
reload() {
this.postResource.reload();
}
}
If you want to cancel any pending request, reload()
it not tool you're looking for.
resource
will cancel any request on dependency changes. Those are expressed as signals in the request
property.
Resource as a writable local state
When using the resource
API, you'll create an use a WritableResource
. This WritableResource
represent a local state
of a resource on your server.
This state is also writable locally, WritableResource.set()
will allow you to programmatically set the local value.
Overwriting the value of a resource sets it to the status
signa to 'local'
.
If you want to expose this resource as readonly, WritableResource.asReadonly()
will return a readonly Ressource
that can be shared with other parts of your application which risking exposing the writable behavior.
RxJS as a first-class citizen: rxResource
It has already been mentioned by the Angular team multiple times, RxJS is a first class citizen and while Angular shouldn't require you to learn/use RxJS, using RxJS with Angular should feel like a natural, polished experience.
As of now, we only discussed resource
as a Promise-based API. v19 will also introduce its Observable
based counterpart: rxResource
from the @angular/core/rxjs-interop
module.
Observable based example
import {rxResource} from '@angular/core/rxjs-interop';
@Component()
export class MyComponent {
postResource: WritableResource = rxResource({
request: () => this.postId(), // this could be one of your signal inputs
loader: (limit) => {
return this.http.get<Post>(`https://dummyjson.com/posts/${postId}`);
},
});
}
The same way as resource
, rxResource
returns a WritableResource
.
Everytime the signals registered in the request
change, the loader (here the http request) will be re-emit and it will update the WritableResource
and its signals.
Single emittion gotcha
It is important to note that if the observable returned to the loader emits multiple value, only the first one will be passed to the resource. This is due to the fact that resource has no way to know when a new request is starting before it actually recieves the new value.
not a pulling example
postResource: WritableResource = rxResource({
request: () => this.postId(), // this could be one of your signal inputs
loader: (limit) => {
return interval(1000).pipe(this.http.get<Post>(`https://dummyjson.com/posts/${postId}`));
},
});
This example will not implement a signal-based pulling. As mentionned before, only the first value will be passed to the resource.
A new request will only be emitted when the postId
signal is emitted.
Last words
The resource
API and its interop counterparts are released as experimental APIs.
This means that they might not become stable at all or have significant changes before becoming stable.
Feel free to report any feedback to the team.