Demonstration Application
For illustrating the various capabilities, I rely on the desserts app that has appeared in earlier posts:

The app lets users look up Austrian desserts by their native German name or by an English equivalent. Users can assign ratings to each dessert or fetch ratings supplied by a supposed authority in the field (who happens to be yours truly).
A separate details screen supports editing a selected dessert:

Getting Started with the Resource API
A resource is defined by a loader function that returns a Promise carrying the fetched data. This loader runs as soon as the resource is set up. Optionally, the resource accepts a params Signal that supplies arguments, such as search filters, to the loader. When that Signal changes, the loader is invoked once more:
@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 params (=search criteria) and loader
// Every time, the params are changing, the loader is triggered
dessertsResource = resource({
params: this.dessertsCriteria,
loader: (loaderParams) => {
return this.#dessertService.findPromise(loaderParams.params);
},
defaultValue: []
});
// initially, resources are undefined
desserts = 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()));
[...]
}
Here, dessertsResource reads the current values of the originalName and englishName Signals. The loader fires right away and again whenever those Signals change. The param object handed to the loader contains the latest criteria gathered from the params Signal.
The outcome of the resource lives in its value Signal. Until the loader resolves asynchronously for the first time, value is undefined unless a defaultValue is supplied — the example sets it to an empty array. The boolean isLoaded exposes the loading status, while error holds any exception that occurred.
The computed Signal ratedDesserts merges the loaded desserts with any fetched ratings. This illustrates a fully reactive pipeline: user input drives the resource, which in turn feeds a model projected into the view. Each action triggers the next one without imperative glue.
Critical Detail: Loaders Are Untracked
One nuance deserves emphasis: although the params are tracked, the loader itself is not. Changing the params Signal causes a fresh loader run, but reading another Signal inside the loader does not.
Auto-tracking would only cover the synchronous segment of the loader. Anything happening after the first asynchronous boundary — for instance, code following await or placed in a .then callback — lies beyond Angular's tracking reach. To avoid inconsistent behavior where parts of the loader are handled differently, the entire loader is intentionally untracked.
Handling Race Conditions
In typical web apps, users can easily trigger multiple overlapping requests. This becomes particularly likely in a reactive interface where changing a filter instantly starts a new fetch:

In such a scenario, the user expects results solely for 'Ice Cream Pancakes', even though different filters were briefly active. Seeing results for plain 'Pancakes' or 'Sacher Cake' flash by would be confusing. The confusion only worsens if the first request happens to take longer:

Then, unwanted intermediate results would appear in an order that mismatches the request sequence. That's the classic race condition.
The Resource API Adopts switchMap Semantics
In Angular, RxJS and its switchMap operator are commonly used to cancel all but the newest overlapping request and thus avoid such scenarios. The good news: resource behaves in the same way. Yet, by default, it cannot abort the earlier request — it merely disregards that request's result.
If you want the previous request to be physically cancelled when a new one is scheduled, you must honor the AbortSignal that the resource API passes to the loader:
[...]
dessertsResource = resource({
params: this.dessertsCriteria,
loader: (loaderParams) => {
return this.#dessertService.findPromise(
loaderParams.params,
loaderParams.abortSignal,
);
},
defaultValue: []
});
[...]
Even though it carries the name AbortSignal, this is not an Angular Signal but a standard JavaScript mechanism available in every modern browser and supported by APIs such as fetch. While Angular's HttpClient can internally rely on fetch, its public API offers no way to pass an AbortSignal. To bridge that gap, I created a small helper function toPromise that turns an Observable into a Promise while honoring an abort signal. The implementation, inspired by the rxResource function mentioned later, is included in the accompanying demo repository.
Although switchMap semantics is the most common choice for data loading, RxJS offers other flattening operators that handle overlapping requests differently. For the operations discussed here, however, resource consistently applies switchMap behavior. The reload method described below, on the other hand, always follows exhaustMap semantics.
This design makes it clear that the Angular team is not aiming to replace existing libraries. Instead, they provide building blocks that cover most scenarios with sensible defaults. When requirements exceed that scope, we can implement custom solutions or bring in battle-tested libraries such as RxJS.
Debouncing Input
Triggering requests immediately after each keystroke or filter change usually calls for debouncing: the user might keep adjusting filters, so waiting a short period avoids firing a burst of redundant requests.
One straightforward approach is to add a delay inside the loader. The downside: the resource enters its loading state during the debounce period. However, in a productive exchange with fellow Angular GDEs and Angular team members (Rainer Hahnekamp, Deborah Kurata, Sander Elias, Matthieu Riegler, and Alex Rickabaugh, among others), we concluded that debouncing the originating event is often the better approach. One reason: debouncing the event prevents the resource from flipping into the loading state before the debounce period finishes. Another: the resource can then be placed into a store that remains agnostic of whether the input is debounced.
When using Reactive Forms, debouncing the underlying event is simple — just apply a debounce operator to the valueChanges Observable. For Template-driven Forms with two-way bindings, a helper that debounces the bound Signal is needed. I wrote a small debounceSignal utility for this purpose:
#debouncedCriteria = debounceSignal(this.#dessertsCriteria, 300);
#dessertsResource = resource({
params: this.#debouncedCriteria,
loader: (loaderParams) => {
return this.#dessertService.findPromise(loaderParams.params, param.abortSignal);
},
defaultValue: []
});
A basic implementation of debounceSignal can rely on Angular's RxJS interop:
import { Signal } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { debounceTime } from "rxjs";
export function debounceSignal<T>(source: Signal<T>, timeMsec: number): Signal<T | undefined> {
return toSignal(toObservable(source).pipe(debounceTime(timeMsec)));
}
Keep in mind: before the first debounce round completes, the resulting Signal holds undefined. A params Signal that returns undefined deliberately prevents the resource from invoking the loader — which is often exactly what we want. If the initial value should bypass debouncing, use the Signal's starting value as the default:
export function debounceSignal<T>(source: Signal<T>, timeMsec: number): Signal<T> {
return toSignal(toObservable(source).pipe(debounceTime(timeMsec)), {
initialValue: source()
});
}
Naturally, a non-RxJS implementation is also possible. That could become increasingly relevant as Angular moves toward making RxJS optional. Regardless, I would treat this kind of functionality as infrastructure — something that belongs in the framework, a helper library, or at least a utility module that the application treats as a black box, rather than scattered across application code. Whether the planned Signal-based Forms will offer a clean answer to this remains to be seen.
Further Learning: Angular Architecture Workshop (online, interactive, advanced)
Take your skills to the next level and master enterprise-scale, maintainable Angular applications with our Angular Architecture workshop!
All Details (English Workshop) | All Details (German Workshop)
Reload and Manual Fetching
The resource shown so far automatically started the loader upon initialization and after each change to the params Signal. That suited the example well, but it's not always the desired behavior.
To suppress the initial loader run, simply have the params Signal return undefined:
type Requested = undefined | true;
[...]
#ratingsRequested = signal<Requested>(undefined);
[...]
#ratingsResource = resource({
params: this.#ratingsRequested,
loader: () => {
return this.#ratingService.loadExpertRatingsPromise();
},
defaultValue: {}
});
readonly ratings = this.#ratingsResource.value;
[...]
As noted earlier, a params Signal returning undefined deliberately skips the loader. In this particular case, no parameters determine which ratings are fetched, so I use the values true or undefined instead.
To kick off the loader for the first time, set the params Signal to true. For subsequent runs, call the resource's reload method:
loadRatings(): void {
this.#ratingsRequested.set(true);
this.#ratingsResource.reload();
}
The reload method guards against overlap — if a request is already in flight, it returns immediately. In RxJS terms, that's exhaustMap behavior. Thanks to this, no distinction is needed between the first and later requests, which keeps loadRatings simple.
Admittedly, relying on a type like undefined | true feels slightly unusual. Still, it aligns with the intended use of the resource: reactive data loading. In a scenario with a genuine parameter — say an expertId — this example would feel more idiomatic.
Interlude 1: linkedSignal for Updates
Users have the ability to modify ratings. Typically, this poses no issue, as a resource can be refreshed manually through its set and update methods, which are covered in an upcoming section. However, a resource's value defaults to undefined. Given that Angular projects default to TypeScript's strict mode, I lean toward avoiding null and undefined in favor of a default object, often referred to as a Null Object. Up to this point, I've relied on computed to map null and undefined to the appropriate Null Object:
ratings = computed(() => this.ratingsResource.value() ?? {});
The drawback is that a computed Signal is read-only. The solution lies in the linkedSignal function, which offers a computed Signal that remains mutable:
ratings = linkedSignal(() => this.ratingsResource.value() ?? {});
When Signals referenced in the computation change, the computation runs again, overwriting any directly assigned value. For completeness, it's worth noting that this computation is lazy: if nobody observes the value, the recalculation is skipped.
This behavior makes linkedSignal ideal for forms: we can modify a local copy and, when ready, push it back for persistence.
Error Handling
Error handling is also integrated into the resource API. If the loader throws or the returned Promise rejects, the resource transitions to the error state. In this state, the error Signal exposes the thrown value or whatever was passed to the reject function of the Promise.
Nevertheless, the resource continues to function: calling the reload method or having the params Signal change triggers the loader again. If the loader succeeds, the resource clears the error Signal and shifts to the resolved state.
This mirrors the typical behavior of Effects in NgRx today, where internal logic prevents the RxJS pipe from terminating when an error occurs.
However, starting with Angular 20, accessing the resource's value is prohibited when an error exists. Consequently, in the template, you must first verify the error state:
@if(!error()) {
@for (dessert of ratedDesserts(); track dessert.id) {
<app-dessert-card
[dessert]="dessert"
(ratingChange)="updateRating(dessert.id, $event)"
></app-dessert-card>
}
}
@else {
<b>Error loading desserts!</b>
}
In this scenario, ratedDesserts is produced from the resource's value via computed. For this reason, an error-state check is necessary here as well.
rxResource for RxJS-Interop
If you already have an Observable, there's no need to convert it into a Promise. Instead, the rxResource API can be employed. It functions similarly to resource, but the loader is expected to return an Observable. Additionally, the loader is defined through the streaming property because an rxResource qualifies as a streaming resource that can emit multiple values over time.
[...]
dessertsResource = rxResource({
params: this.dessertsCriteria,
stream: (loaderParams) => {
return timer(300).pipe(switchMap(() => this.#dessertService.find(loaderParams.params)));
},
defaultValue: []
});
[...]
ratingsResource = rxResource({
params: this.#ratingsRequested,
stream: () => {
return this.#ratingService.loadExpertRatings()
},
defaultValue: {}
});
[...]
The loader also doesn't require the AbortSignal, since Observables can generally be cancelled through implicit or explicit unsubscription.
Resource in Services and Stores
The examples so far have used the resource directly within a component, mainly for simplicity. In practice, resources are often placed in services or stores. One motivation is state management: loaded data persists even when Angular destroys the current component (for instance, due to route changes), allowing it to be reused by another instance of the same or a different component later.
The second motivation is that stores help organize your reactive dataflow effectively:

This creates what's known as unidirectional data flow: components send intentions to the store. I use "intention" loosely because its expression depends on the store implementation. In a Redux-style store, it manifests as a dispatched action; in a lighter store like the NGRX Signal Store, it might simply be an invoked method.
The expressed intention prompts the store to execute tasks that produce new values placed into Signals. These Signals can be projected via computed and deliver data down to the component's view.
A straightforward store is a service that exposes Signals. This means we can relocate the code discussed thus far into a service:
type Requested = undefined | true;
@Injectable({ providedIn: 'root' })
export class DessertStore {
#dessertService = inject(DessertService);
#ratingService = inject(RatingService);
private #ratingsRequested = signal<Requested>(undefined);
readonly originalName = signal('');
readonly englishName = signal('');
#dessertsCriteria = computed(() => ({
originalName: this.originalName(),
englishName: this.englishName(),
}));
#dessertsResource = resource({
params: this.#dessertsCriteria,
loader: debounce((loaderParams) => {
return this.#dessertService.findPromise(loaderParams.params, loaderParams.abortSignal);
}),
defaultValue: []
});
#ratingsResource = resource({
params: this.#ratingsRequested,
loader: () => {
return this.#ratingService.loadExpertRatingsPromise();
},
defaultValue: {}
});
readonly loading = computed(() => this.#ratingsResource.isLoading() || this.#dessertsResource.isLoading());
readonly desserts = this.#dessertsResource.value;
readonly ratings = this.#ratingsResource.value;
readonly ratedDesserts = computed(() => toRated(this.desserts(), this.ratings()));
readonly error = computed(() => getErrorMessage(this.dessertsResource.error() || this.ratingsResource.error()));
loadRatings(): void {
this.#ratingsRequested.set(true);
this.#ratingsResource.reload();
}
updateRating(id: number, rating: number): void {
this.#ratingsResource.update((ratings) => ({
...ratings,
[id]: rating,
}));
}
}
Here's the component consuming the store:
@Component([...])
export class DessertsComponent {
#store = inject(DessertStore);
originalName = this.#store.originalName;
englishName = this.#store.englishName;
loading = this.#store.loading;
error = this.#store.error;
ratedDesserts = this.#store.ratedDesserts;
loadRatings(): void {
this.#store.loadRatings();
}
updateRating(id: number, rating: number): void {
this.#store.updateRating(id, rating);
}
}
In this setup, the unidirectional data flow is evident: the component communicates its intentions by invoking methods and receives (refreshed) data through Signals bound in the template.
Updating Resources
Resources are not limited to read-only operations; they can be updated with new values. While persisting changed values requires manual effort, locally updating the resource ensures the new value integrates into our reactive flow. This means it gets displayed and projected, serving as the basis for subsequent changes.
The example below illustrates a simple service acting as a store for a details view. Pay close attention to the save method:
@Injectable({ providedIn: 'root' })
export class DessertDetailStore {
#dessertService = inject(DessertService);
#id = signal<number | undefined>(undefined);
#dessertResource = resource({
params: computed(() => ({ id: this.#id() })),
loader: async (loaderParams) => {
const id = loaderParams.params.id;
if (id) {
const result = await this.#dessertService.findPromiseById(id);
return result ?? initDessert;
}
else {
return Promise.resolve(initDessert);
}
},
defaultValue: 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 receives an updated dessert and sends it back to the server. The HTTP call is only hinted at with a comment here. After this call completes, the dessertResource is refreshed with the new value via set. Alternatively, an update method exists that lets you project the current value into a new one. If a loading process is in progress when the resource is updated with such a local value, that loading process gets cancelled.
In the example above, the resource remains private to the service. This allows the service to guarantee proper updates of the resource. To expose the entire resource, you can leverage its asReadonly method to prevent consumers from modifying the managed data:
readonly dessert = dessertResource.asReadonly();
Interlude 2: linkedSignal for the Details Form
When connecting the loaded dessert to a template-driven form, writable Signals are required. However, stores typically expose read-only Signals. Additionally, the value Signal from resource is read-only, despite being directly changeable through the resource's set and update methods.
As noted earlier, linkedSignal fits this scenario well:
@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 bound to a form field is represented by a linked Signal. They refresh whenever their source changes, yet they can still be set with local values. The save method captures this local value and forwards it to the store, which delegates to the resource.
As a result, we can bind ngModel directly 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>
Think of these linked Signals as the counterpart to FormControl objects in reactive forms. Naturally, FormControl objects offer additional features such as validator registration. But, in my view, this demonstrates that with Signals, both approaches are quite close in nature.
While I opted for template-driven forms here, reactive forms are entirely viable too. In that case, you might consider an effect to bridge the Signals from the store to your FormControls.
Bonus: Helper Functions for Streamlining
While building the demo application featured here, I also developed some reactive helpers to simplify working with Signals and resources. These are available in the provided demo repository under the branch 07c-final. One such helper, deepLink, takes a Signal containing an Object and transforms all its properties into linked Signals:
dessert = deepLink(this.loadedDessert);
This avoids the need to call linkedSignal for each property individually, as demonstrated in the previous section.
Additionally, I aimed to suppress the loading indicator for quick operations. Thus, 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 shift from true to false, because we generally want the loading indicator to vanish immediately once the loaded resource is ready.
Conclusion
The new Resource API indeed fills a gap in Angular's Signal ecosystem: it provides an official mechanism for loading asynchronous resources directly within the reactive flow. It also manages race conditions and includes basic error handling.
With this API, common use cases gain a built-in, easy-to-use feature. For more complex scenarios like handling multiple parallel data streams, more powerful tools such as RxJS can be employed. Thanks to the RxJS interop and rxResource, bridging both worlds is straightforward.
