About a month before the release of version 19, Angular's Alex Rickabaugh published a PR for a new reactive API. The so-called Resource API allows for asynchronously loading resources using Signals. A typical use case for resource
is loading data via HTTP.
In this article, I show how to build a typical CRUD scenario using the new resource
API. As it has not been released at the time of writing, my examples directly use the code from the PR. I will update this article when resource
lands in Angular to reflect possible updates and changes.
📂 Source Code (see Branch 07c-final
)
Updates:
- 2024-11-01: Update for Angular 19 RC 0
Example Application
To demonstrate the individual features, I'm using the desserts application already known from former articles:
It basically allows searching for Austrian desserts using the original Austrian German name or its English translation. You can rate the individual desserts or load the ratings provided by a recognized expert in this field (who happens to be my lesser self).
Also, there is a details view for editing a dessert:
First Steps with the Resource API
Each resource
has a loader
function that returns a Promise
with the loaded data. This loader
is triggered when the resource has been initialized. Optionally, the resource can have a request
Signal providing parameters (search criteria) for the loader
. Every time the request
Signal changes, the loader is triggered again:
@Component([...])
export class DessertsComponent {
#dessertService = inject(DessertService);
[...]
// Criteria for search
originalName = signal('');
englishName = signal('');
// Combine criteria to computed Signal
dessertsCriteria = computed(() => ({
originalName: this.originalName(),
englishName: this.englishName(),
}));
// Define resource with request (=search criteria) and loader
// Every time, the request (criteria) is changing, the loader is triggered
dessertsResource = resource({
request: this.dessertsCriteria,
loader: (param) => {
return this.#dessertService.findPromise(param.request);
}
});
// initially, resources are undefined
desserts = computed(() => this.dessertsResource.value() ?? []);
loading = this.dessertsResource.isLoading;
error = this.dessertsResource.error;
// The reactive flow goes on ...
ratings = signal<DessertIdToRatingMap>({});
ratedDesserts = computed(() => this.toRated(this.desserts(), this.ratings()));
[...]
}
In this example, dessertsResource
uses the values in the originalName
and englishName
Signals as parameters. The loader is triggered initially and when these Signals change. The param
object passed to the loader
contains the current search criteria from the request
Signal.
The resource
's result is found in its value
Signal. The computed desserts
Signal switches out the initial undefined
value by an empty array. The boolean isLoaded
informs about the loading state, and error
is a Signal with a possible error that occurred during loading.
The computed ratedDesserts
Signal combines the received desserts with possibly loaded ratings. This shows that we have a clear reactive flow that extends from the user input through loading the resource to projecting the resource to a model bound in the view. One action reactively leads to another.
Important: Loaders are Untracked!
It's important to note that while the request
is tracked, the loader
isn't. That means that a change in the request
Signal triggers a new loader
execution, but a change in a Signal
used in the loader
does not.
This is because auto-tracking would only work in the first part of the loader
that is executed synchronously. Everything that is executed after the first asynchronous operation, e.g., code after await
or in a .then
handler, cannot be subject to Angular's auto-tracking. To avoid such confusing situations where a part of the loader is handled differently, the whole loader is untracked.
Race Conditions
In many web applications, the user can easily trigger overlapping requests. This is especially the case in an reactive UI where just changing a filter can trigger a further loading operation:
In this case, the user would expect to only get results for 'Ice Cream Pancakes' although they shortly had different filters before. It would be quite confusing if results for ordinary 'Pancakes', and 'Sacher Cake' briefly flashed. It would be even more confusing if the first search took a bit longer:
In this case, the unwanted intermediate results would flash in an order that does not match the order of requests. You call this a race condition.
The Resource API has switchMap Semantics
In Angular, we usually leverage RxJS and its switchMap
Operator that cancels overlapping requests but the last one and hence prevents such situations. The good news is that resource
uses the same behavior. However, by default it cannot cancel the former request. Instead, it just ignores its result.
If you really want to cancel the former request when another one is scheduled, you need to respect the AbortSignal
the resource
API passes into the loader
:
[...]
dessertsResource = resource({
request: this.dessertsCriteria,
loader: (param) => {
return this.#dessertService.findPromise(
param.request,
param.abortSignal,
);
},
});
[...]
Although it's called AbortSignal,
it's not a Signal in the sense of Angular's reactivity primitives but a JavaScript API provided by all modern browsers and supported by several APIs such as the Fetch API. While Angular's HttpClient
is meanwhile capable of using fetch
under the hoods, its API does not allow passing in an AbortSignal
. To compensate for this, I've written a simple helper function toPromise
that converts an Observable
to a Promise
respecting an AbortSignal
. You find this implementation that is inspired by the rxResource
function mentioned below, in the provided demo repository.
While switchMap
semantics might be the most common one used for loading data, RxJS provides further so-called flattening operators dealing with overlapping requests in different ways. However, for the discussed operations, resource
always uses switchMap
semantics. Besides this, the reload
method discussed below always uses exhaustMap
sematics.
This shows that the Angular team does not want to compete with existing libraries. The goal is to provide building blocks with reasonable default logic covering the majority of use cases. For everything that goes beyond that, we can implement our own solutions or add proven libraries like RxJS.
Debouncing
Usually, when triggering requests directly after the user changes a filter, we want to have some debouncing: Chances are, the user is changing further filters, and hence, we want to wait some moments to not trigger several unnecessary requests in a row.
As the resource loader is returning a Promise
that is aborted (or ignored) when a subsequent overlapping request comes in, we just need to add a Promise
-based version of setTimeout
:
dessertsResource = resource({
request: this.dessertsCriteria,
loader: async (param) => {
// Debouncing: 300 ms
await wait(300, param.abortSignal);
return await this.#dessertService.findPromise(
param.request,
param.abortSignal,
);
},
});
Please note that I switched to using await
and, hence, to async
to ensure that the resource is loaded after the debouncing. The function wait
is a Promise-based wrapper for setTimeout
respecting the passed AbortSignal
. You find its implementation in the provided repository.
More on this: Angular Architecture Workshop (online, interactive, advanced)
Become an expert for enterprise-scale and maintainable Angular applications with our Angular Architecture workshop!
All Details (English Workshop) | All Details (German Workshop)
Reload and Manual Loading
The resource
used so far automatically triggered the loader
after its initialization and after each change of the request
Signal. This was convenient in the example at hand. However, it's not a behavior you want all the time.
If you want to disable the initial loader execution, your loader can check if the resource
is in idle
state. This is the initial state and also the state it transitions to when it is destroyed. Alternatively, you can also use a class property with a flag telling you if the loader was called for the first time.
To disable automatic subsequent loader executions, you can just skip the request
Signal. Our demo application disables both initial loading and subsequent for loading ratings:
ratingsResource = resource({
// no request Signal === no automatic subsequent loading
loader: (param) => {
if (param.previous.status === ResourceStatus.Idle) {
// skip initial loading!
return Promise.resolve(undefined);
}
return this.#ratingService.loadExpertRatingsPromise();
}
});
To trigger loading manually, we can call the resource
's reload
method. This allows us to provide a button for this task:
loadRatings(): void {
this.ratingsResource.reload();
}
The reload
method prevents overlapping requests by immediately returning if there is already a running one. Using terms from RxJS, this can be described as exhaustMap
semantics.
Interlude 1: linkedSignal for Updates
The user can change the ratings. Normally, this is fine because a resource can be manually updated with its set
and update
methods used in a below section. However, a resource
's value
is undefined
by default. At least since Angular applications use TypeScript's strict mode by default, I try to avoid null
and undefined
in favor of a default object, also known as Null Object. So far, I used computed
to project null
and undefined
to the respective Null Object:
ratings = computed(() => this.ratingsResource.value() ?? {});
Unfortunately, a computed Signal is read only. The rescue is the linkedSignal
function. It provides a computed Signal that can be changed:
ratings = linkedSignal(() => this.ratingsResource.value() ?? {});
If Signals used in the computation change, the computation is retriggered and the directly assigned value is overwritten. For the sake of completeness, I want to mention that the computation happens lazily: If no one is interested in the value, the recomputation does not happen.
This characteristic makes linkedSignal
a perfect fit for forms: We can change a local copy and, on-demand, send it back for saving.
Error Handling
Also Error-Handling is baked into the resource
API. When the loader throws or rejects the returned Promise
, the resource
switches into the state error
. In this state, the error
Signal provides the thrown value or the value passed to the Promise
's reject function.
However, the resource
will still proceed to work: If someone calls the reload
method or if the request
is changing, the loader
is executed again. If the loader
succeeds, the resource clears the error
Signal and moves to the resolved
state.
This is comparable to today's default behavior of Effects in NgRx, where some additional logic under the covers prevents the RxJS pipe used from stopping in the case of an error.
rxResource for RxJS-Interop
If you already have an Observable, you don't need to convert it into a Promise
. Instead, you can use the rxResource
API. It works like the resource
but the loader is expected to return an Observable
:
[...]
dessertsResource = rxResource({
request: this.dessertsCriteria,
loader: (param) => {
return timer(300).pipe(switchMap(() => this.#dessertService.find(param.request)));
}
});
[...]
ratingsResource = rxResource({
loader: rxSkipInitial(() => {
return this.#ratingService.loadExpertRatings()
})
});
[...]
Also, the loader does not get an AbortSignal
passed as Observables can be aborted (by unsubscribing implicitly or explicitly) in general.
It's important to know that the current implementation of rxResource
is only using the first emitted value. Internally, it leverages firstValueFrom
to convert the Observable
to a Promise
before handing over to resource
. While this behavior might be ok for HTTP calls, it will result in a surprise when several values are received via a stream, e.g., when consuming Web Sockets.
We will see if this behavior will be changed over time. So far, I would use Observables emitting several values in a traditional way.
Resource in Services and Stores
The shown examples directly used the resource
in a component. This was primarily for the sake of simplicity. Often, you will use resources in services or stores. One reason for this is state management: The loaded data will survive when Angular destroys the component at hand (e.g., because of switching to another route) so that it can be used later by an instance of the same or another component.
The second reason is that stores help to streamline your reactive dataflow:
This result in a so-called unidirectional data flow: The components send intentions to the store. I'm using the term 'intension' in an abstract way because it depends on the store, how it is expressed. When using a Redux store, it will be a triggered action; in a more lightweight store like the NGRX Signal Store, it might just be a called method.
The expressed intention makes the store perform some tasks that result in new values put into Signals. These Signals can be projected via computed
and transport the data down to the component's view.
A very simple store is a service providing Signals. That means, we can move the code we've discussed so far into a service:
@Injectable({ providedIn: 'root' })
export class DessertStore {
#dessertService = inject(DessertService);
#ratingService = inject(RatingService);
readonly originalName = signal('');
readonly englishName = signal('');
#dessertsCriteria = computed(() => ({
originalName: this.originalName(),
englishName: this.englishName(),
}));
#dessertsResource = resource({
request: this.#dessertsCriteria,
loader: debounce((param) => {
return this.#dessertService.findPromise(param.request, param.abortSignal);
})
});
#ratingsResource = resource({
loader: skipInitial(() => {
return this.#ratingService.loadExpertRatingsPromise();
})
});
readonly loading = computed(() => this.#ratingsResource.isLoading() || this.#dessertsResource.isLoading());
readonly desserts = computed(() => this.#dessertsResource.value() ?? []);
readonly ratings = computed(() => this.#ratingsResource.value() ?? {});
readonly ratedDesserts = computed(() => toRated(this.desserts(), this.ratings()));
loadRatings(): void {
this.#ratingsResource.reload();
}
updateRating(id: number, rating: number): void {
this.#ratingsResource.update((ratings) => ({
...ratings,
[id]: rating,
}));
}
}
Here, we see the component consuming the store:
@Component([...])
export class DessertsComponent {
#store = inject(DessertStore);
originalName = this.#store.originalName;
englishName = this.#store.englishName;
loading = this.#store.loading;
ratedDesserts = this.#store.ratedDesserts;
loadRatings(): void {
this.#store.loadRatings();
}
updateRating(id: number, rating: number): void {
this.#store.updateRating(id, rating);
}
}
In this example, the unidirectional data flow can be seen quite clearly: The component sends up its intentions by calling methods and receives (updated) data via Signals bound in the template.
Updating Resources
Resources are not just read-only. We can also update them with a new value. While writing back changed values needs to be done manually, locally updating the resource ensures that the new value becomes part of our reactive flow. That means it will be displayed and projected, and it will be the foundation for further updates.
The following example shows a simple Service acting as a store for a details view. Please pay particular attention to the save
method:
@Injectable({ providedIn: 'root' })
export class DessertDetailStore {
#dessertService = inject(DessertService);
#id = signal<number | undefined>(undefined);
#dessertResource = resource({
request: computed(() => ({ id: this.#id() })),
loader: (param) => {
const id = param.request.id;
if (id) {
return this.#dessertService.findPromiseById(id);
}
else {
return Promise.resolve(initDessert);
}
},
});
readonly dessert = computed(() => this.#dessertResource.value() ?? initDessert);
readonly loading = this.#dessertResource.isLoading;
readonly error = this.#dessertResource.error;
#saving = signal(false);
load(id: number): void {
this.#id.set(id);
}
save(dessert: Dessert): void {
try {
this.#saving.set(true);
console.log('saving', dessert);
// Here would be your HTTP Call
[...]
this.#dessertResource.set(dessert);
}
finally {
this.#saving.set(false);
}
}
}
The save
method gets an updated dessert and writes it back to the server. The respective HTTP call is only indicated with a comment here. After performing this call, the dessertResource
is updated with the new value using set
. As an alternative, there is also an update
method that allows to project the current value to a new one. If a loading process takes place while updating the resource with such a local value, this loading process is aborted.
In the example above, the resource
was private to the service. Hence, the service can make sure the resource is updated only properly. If you want to expose the whole resource
, you can use its asReadonly
method to prevent consumers from changing the managed data:
readonly dessert = dessertResource.asReadonly();
Interlude 2: linkedSignal for the Details Form
When binding the loaded dessert
to a template-driven form, we need writable Signals. However, stores usually provide read-only Signals. Also, the value
Signal provided by resource
is read-only, although it can be changed directly via the resource
's set
and update
methods.
As already mentioned above, this is a good case for linkedSignal
:
@Component({
selector: 'app-dessert-detail',
standalone: true,
imports: [JsonPipe, RouterLink, FormsModule],
templateUrl: './dessert-detail.component.html',
styleUrl: './dessert-detail.component.css'
})
export class DessertDetailComponent implements OnChanges {
store = inject(DessertDetailStore);
id = input.required({
transform: numberAttribute
});
loadedDessert = this.store.dessert;
loading = this.store.loading;
error = this.store.error;
dessert = {
originalName: linkedSignal(() => this.loadedDessert().originalName),
englishName: linkedSignal(() => this.loadedDessert().englishName),
kcal: linkedSignal(() => this.loadedDessert().kcal)
};
ngOnChanges(): void {
const id = this.id();
this.store.load(id);
}
save(): void {
const dessert = {
...this.loadedDessert(),
originalName: this.dessert.originalName(),
englishName: this.dessert.englishName(),
kcal: this.dessert.kcal(),
};
this.store.save(dessert);
}
}
Here, each property we want to bind to a form field is represented by a linked Signal. They are updated whenever their source is changing. Nevertheless, they can be updated with a local value. The save
method takes this local value and writes it back to the store which delegates to the resource
.
As a result, we can directly bind ngModel to our Signals:
<form>
<div>
<label for="englishName"> English Name </label>
<input name="englishName" [(ngModel)]="dessert.englishName" />
</div>
<div>
<label for="originalName"> OriginalName </label>
<input name="originalName" [(ngModel)]="dessert.originalName" />
</div>
<div>
<label for="kcal"> kcal </label>
<input name="kcal" [(ngModel)]="dessert.kcal" />
</div>
</form>
You can see these linked Signals as the counter part of the FormControl
objects in reactive forms. Of course, FormControl
objects provide more features like registering validators. But, and this is my personal impression, this shows that with Signals both approaches are not that far from each other.
While I decided to use template-driven forms in this example, you can totally leverage reactive forms, too. In this case, you might want to use an effect
for connecting the Signals obtained from the store to your FormControl
s.
Bonus: Helper Functions for Streamlining
When implementing the demo application shown here, I also wrote some reactive helpers streamlining the work with Signals and resources. You find them in the provided demo repository in the branch 07c-final
. For instance, the deepLink
takes a Signal with an Object and converts all its properties to linked Signals:
dessert = deepLink(this.loadedDessert);
This prevents us from calling linkedSignal
once per property, as in the example found in the previous section.
A debounce helper wrapping the loader takes care of debouncing:
dessertsResource = resource({
request: this.dessertsCriteria,
loader: debounce((param) => {
return this.#dessertService.findPromise(param.request, param.abortSignal);
})
});
It also respects the current AbortSignal
. The default debounce time is 300 msec, but there is a parameter allowing us to specify another value.
Another helper is skipInitial
which skips the initial call to the loader:
ratingsResource = resource({
loader: skipInitial(() => {
return this.#ratingService.loadExpertRatingsPromise();
})
});
As a result, only subsequent changes to a potentially existing request or calls to reload
trigger (re)loading.
Also, I wanted to prevent the loading indicator from showing up for quick loading operations. Hence, my debounceTrue
helper uses a resource to debounce a transition from false
to true
:
loading = debounceTrue(() => this.ratingsResource.isLoading() || this.dessertsResource.isLoading(), 500);
It does not debounce a transition from true
to false
because we usually want the loading indicator to disappear immediately when the loaded resource becomes available.
Conclusion
The new Resource API is indeed a missing link in Angular's Signal story: It gives us an official way for loading asynchronous resources directly within the reactive flow. Also, it takes care of race conditions and contains basic error handling.
With this new API, we have an easy-to-use built-in feature for very common use cases. For more advanced scenarios, such as working with several parallel data streams, we can switch to something more powerful like RxJS. Thanks to the RxJS interop and rxResource
, both worlds can be bridged.