When Angular v16 introduced signals, the framework set out on a new path toward rethinking reactivity and change detection. The developer community embraced this addition warmly, yet it was clear that signals, in their initial form, were still in a raw state and needed more reactive primitives to unlock their full potential.
The fundamental contrast between signals and RxJS (which Angular has long supported) is that Observables are predominantly used for asynchronous operations — though they can also be synchronous — whereas signals are inherently synchronous.
Given this distinction, the need to bridge the two paradigms became apparent, and that is how resource and rxResource came to be. We will explore these reactive primitives through actionable examples, so you can feel at ease using them in your routine work.
Note:
resourceandrxResourceare experimental, meaning that the core team does not encourage their use in production ready applications. An RFC from the Angular team is planned to discuss all the outstanding issues. The API of these functions can change drastically in the future. This article will be updated to have links to more up-to-date resources.
The problem
Fetching data from a server and rendering it in the UI is one of the most frequent tasks in front-end development. Previously, we would write something along these lines:
@Component({
template: `
<div *ngIf="data">
<h1>{{data.title}}</h1>
<p>{{data.description}}</p>
</div>
`
})
export class MyComponent implements OnInit {
readonly #dataService = inject(DataService);
data: SomeData = null;
ngOnInit() {
this.#dataService.getData().subscribe(data => this.data = data);
}
}
This approach handled the basics, yet it was wordy and rigid. Components dealing with substantial data ended up with numerous disconnected fragments of code; properties were declared empty only to be populated later inside subscribe callbacks located in ngOnInit or elsewhere.
Developers who were more experienced circumvented these obstacles by leveraging the async pipe, allowing Observable values to be read directly in the template. Here is the same scenario rewritten with async:
@Component({
template: `
<div *ngIf="data$ | async as data">
<h1>{{data.title}}</h1>
<p>{{data.description}}</p>
</div>
`
})
export class MyComponent implements OnInit {
readonly #dataService = inject(DataService);
data$ = this.#dataService.getData();
}
This cuts down on the verbosity, but it still leaves many aspects of data fetching unhandled. What about displaying a loading spinner? How do we handle errors? What about retry logic, or the case where we want to delay the request until some signal changes or an event such as a button click occurs?
Angular developers adopted a wide range of strategies to deal with this; some utilized RxJS operators like switchMap and retry, while others turned to community-driven solutions such as the derivedAsync function from the ngxtension package.
Regardless, it was about time Angular shipped its own native mechanism to solve these problems.
The solution
Angular v19 introduced two functions, resource and rxResource. Functionally, they accomplish the same goals; the only real difference is that resource integrates with Promises, whereas rxResource integrates with Observables.
Given that Angular's HTTP client relies on RxJS, let us focus on rxResource, keeping in mind that everything discussed applies equally if we chose fetch with resource instead of HttpClient.get with rxResource.
Loading data
Let us construct a functional page using rxResource. We will rely on the Rest Countries API, a free service offering comprehensive details about the world's nations. Starting simple, we will just render every country on a single page.
export type Country = {
name: {
official: string;
};
// other properties
};
@Component({
template: `
@if (countriesResource.status() === status.Resolved) {
<ul>
@for (
country of countriesResource.value();
track country.name.official
) {
<li>{{ country.name.official }}</li>
}
</ul>
}
`,
})
export class AppComponent {
readonly #http = inject(HttpClient);
status = ResourceStatus;
countriesResource = rxResource({
loader: () =>
this.#http.get<Country[]>('https://restcountries.com/v3.1/all'),
});
}
Let us walk through what is happening here:
- We use the
HttpClientto retrieve the list of countries. This is straightforward. - The
rxResourcefunction takes a configuration object which includes aloaderfunction. - This loader function is what carries out the actual request. We can place any logic inside, though typically this will be a call to
HttpClient.get. - We also bring in the
ResourceStatusenum, which enumerates the possible resource states. - Within the template, we first verify whether the resource has been resolved (
countriesResource.status() === status.Resolved). - We then read the resource values. The
valuewe access is actually a signal that populates once the resource is resolved.
Now, the status signal carries rich information about the resource's state; however, it is not always ideal for checking whether the data is ready to be shown. We will examine some of these distinctions later, but for now let us modify the example to utilize another signal known as hasValue, which becomes truthy when the resource has completed loading and the data can be accessed:
@if (countriesResource.hasValue()) {
<ul>
@for (
country of countriesResource.value();
track country.name.official
) {
<li>{{ country.name.official }}</li>
}
</ul>
}
This covers all the pending cases. As we can observe, every piece related to this HTTP request is bundled inside the countriesResource object. Let us see what other capabilities we have at our disposal.
Handling errors
So, how can we determine when a request fails? And how do we tap into the actual error message or object? It is straightforward — we rely on the error signal together with the status. Here is the revised code:
@if (countriesResource.hasValue()) {
<ul>
@for (
country of countriesResource.value();
track country.name.official
) {
<li>{{ country.name.official }}</li>
}
</ul>
} @else if (countriesResource.status() === status.Error) {
<span>The request failed. Reason: {{ countriesResource.error() }}</span>
}
Notice that we only had to adjust the template to cover the failure case. The ResourceRef instance returned by rxResource carries everything we need to know whether the request was unsuccessful, along with the precise error, courtesy of the error signal.
Retrying failed requests
What happens when we want to retry? For instance, we might want to show a button that triggers the request again.
@if (countriesResource.hasValue()) {
// omitted for the sake of brevity
} @else if (countriesResource.status() === status.Error) {
<span>The request failed. Reason: {{ countriesResource.error() }}</span>
<button (click)="countriesResource.reload()">Retry</button>
}
The ResourceRef exposes a reload method; in essence, it reruns the same loader function we supplied, enabling us to attempt a retry when encountering an error, or simply reload the data whenever we see fit.
Loading State
Another recurring concern with HTTP requests involves showing some form of loading indicator — a spinner or at minimum a textual cue. Naturally, this is also trivial to accomplish with a resource:
@if (countriesResource.hasValue()) {
// omitted for the sake of brevity
} @else if (countriesResource.status() === status.Error) {
<span>The request failed. Reason: {{ countriesResource.error() }}</span>
<button (click)="countriesResource.reload()">Retry</button>
} @else if (
countriesResource.status() === status.Loading ||
countriesResource.status() === status.Reloading
) {
<span>Loading Countries...</span>
}
There are two distinct statuses here, Loading and Reloading, distinguishing between the initial data fetch and any subsequent load. This can be beneficial in contexts where we need to tell the two apart.
In generic scenarios, this feels a bit clunky and becomes outright problematic when we want to update the resource value manually (we will see this is possible later). In those cases, the status would be ResourceStatus.Local, indicating the value has been modified through some user-driven action. To sidestep juggling multiple checks, ResourceRef offers a convenient signal called isLoading, giving us a clear indication of whether the loading UI is needed. Let us refactor the code to see how this plays out:
@if (!countriesResource.isLoading()) {
// omitted for the sake of brevity
} @else if (countriesResource.status() === status.Error) {
<span>The request failed. Reason: {{ countriesResource.error() }}</span>
<button (click)="countriesResource.reload()">Retry</button>
} @else {
<span>Loading Countries...</span>
}
Before we proceed, let us consider what would change if, for some reason, we wanted to set RxJS aside entirely and work with Promises. We obviously could not use HttpClient anymore, since it invariably returns Observables; instead of rxResource, we would use resource:
export class AppComponent {
readonly #http = inject(HttpClient);
status = ResourceStatus;
countriesResource = resource({
loader: () => {
// throw new Error('X');
return fetch('https://restcountries.com/v3.1/all')
.then(res => res.json()) as Promise<Country[]>;
},
});
}
A simple change, and notably, everything else stays intact. Given that most Angular codebases rely on HttpClient (and thus RxJS) for HTTP, for the remainder of this article we will stick with rxResource; you can safely assume that anything we mention also applies to resource.
Now it is time to look at more advanced scenarios.
Re-evaluating loaded data based on other signals
Filtering, sorting and paginating server-side data is a widespread requirement. Typically, we have some inputs the user can interact with to set the page, define sorting parameters, and similar attributes; whenever those change, we want to refetch the data.
rxResource handles this naturally, since it accepts another configuration parameter: a function returning the request parameters to be used. Imagine we add a search box to our page so we can filter countries by name.
@Component({
template: `
<input [(ngModel)]="countryName" placeholder="Filter by name"/>
@if (!countriesResource.isLoading()) {
<ul>
@for (
country of countriesResource.value();
track country.name.official
) {
<li>{{ country.name.official }}</li>
}
</ul>
} @else if (countriesResource.status() === status.Error) {
<span>The request failed. Reason: {{ countriesResource.error() }}</span>
<button (click)="countriesResource.reload()">Retry</button>
} @else {
<span>Loading Countries...</span>
}
`,
})
export class AppComponent {
readonly #http = inject(HttpClient);
countryName = signal('');
status = ResourceStatus;
countriesResource = rxResource({
request: () => ({ name: this.countryName() }),
loader: (parameters) => {
return this.#http.get<Country[]>(
`https://restcountries.com/v3.1/name/${parameters.request.name}`,
);
},
});
}
Two changes are made here; first, we introduce a countryName signal and connect it to the input via [(ngModel)]. Next, we add a request function to the resource configuration. This function computes the request parameters; importantly, if we read any signals within it, it will be re-executed whenever those signals change. The signals inside that callback are tracked just like they would be in a computed or effect.
This implies that we defined an entire lifecycle in a single line of code:
- The user types a country name in the input
- The signal gets updated via
[(ngModel)] - This triggers re-evaluation of the
requestfunction - This prompts re-execution of the
loaderfunction, issuing a fresh HTTP call based on the new parameters - This causes updates to the
countriesResourceobject, transitioning its status toLoading(thus showing the spinner or text) and then toResolved/Error, determined by the request outcome
All of this is achieved merely by providing a function that describes which reactive parameters our request is dependent on.
Additionally, when using resource (and hence fetch), the first argument of the request callback (named parameters in our example) will contain an abort signal, useful for cancelling the request if required!
export class AppComponent {
readonly #http = inject(HttpClient);
countryName = signal('');
status = ResourceStatus;
countriesResource = resource({
request: () => ({ name: this.countryName() }),
loader: (parameters) => {
// throw new Error('X');
return fetch(
`https://restcountries.com/v3.1/name/${parameters.request.name}`,
{signal: parameters.abortSignal}
).then(res => res.json()) as Promise<Country[]>;
},
});
}
With this in place, we can delegate to a specific ResourceRef method to cancel the request:
<button (click)="countriesResource.destroy()">Cancel</button>
This abort signal exists both in resource and rxResource; however, for rxResource it is unnecessary for cancellation and can be set aside. With rxResource, we still have the option to invoke destroy on the ResourceRef object returned; this simply unsubscribes from the Observable we supplied, thereby cancelling the request.
Some caveats
That said, running this exact code (or the previous resource variant) with this particular API URL will produce an error initially. This occurs because the API expects a name; the initial value is an empty string, causing the request to fail. How can we handle this? This is crucial in situations where we prefer not to fire the request immediately, but rather wait for a signal change, exactly as in this example.
In this simple case, we can verify whether the name parameter holds any content; if it does not, we return an Observable of an empty array:
export class AppComponent {
readonly #http = inject(HttpClient);
countryName = signal('');
status = ResourceStatus;
countriesResource = rxResource({
request: () => ({ name: this.countryName() }),
loader: (parameters) => {
if (parameters.request.name === '') {
return of([]);
}
return this.#http.get<Country[]>(
`https://restcountries.com/v3.1/name/${parameters.request.name}`
);
},
});
}
Note: with
resourcewe can usePromise.resolve([])instead ofof([])
Additionally, when more nuanced comparisons are necessary, the parameters object offers a previous property holding the previous request parameters. This proves handy in situations with numerous source signals, such as reloading the data only when the user changes a particular value while ignoring other changes.
Using the data locally
One further advantage of rxResource/resource is that they expose writable signals, allowing us to modify them or bind them to form controls with [(ngModel)]. This is immensely useful for a typical scenario: fetching data from the server so the user can subsequently edit it. Here is a brief, generalized illustration:
export class SomeComponent {
readonly #http = inject(HttpClient);
someValueResource = resource({
loader: () => this.#http.get<string>('https://some-url.com'),
});
updateSomeValue() {
this.someValue.set('Some new value');
}
}
As demonstrated, ResourceRef is a writable signal; we can update the resource's value without necessarily reloading it from the server.
We can enhance our example to support the user removing a country by clicking an "X" button next to each entry:
@Component({
selector: 'app-root',
standalone: true,
template: `
<input [(ngModel)]="countryName" placeholder="Filter by name"/>
@if (!countriesResource.isLoading()) {
<ul>
@for (
country of countriesResource.value();
track country.name.official;
) {
<li>
{{ country.name.official }}
<button (click)="deleteCountry(country.name.official)">X</button>
</li>
}
</ul>
} @else if (countriesResource.status() === status.Error) {
<span>The request failed. Reason: {{ countriesResource.error() }}</span>
<button (click)="countriesResource.reload()">Retry</button>
} @else {
<span>Loading Countries...</span>
}
`,
imports: [FormsModule],
})
export class AppComponent {
readonly #http = inject(HttpClient);
countryName = signal('');
status = ResourceStatus;
countriesResource = rxResource({
request: () => ({ name: this.countryName() }),
loader: (parameters) => {
if (parameters.request.name === '') {
return of([]);
}
return this.#http.get<Country[]>(
`https://restcountries.com/v3.1/name/${parameters.request.name}`
);
},
});
deleteCountry(name: string) {
this.countriesResource.update((countries) => {
return countries?.filter(
(country) => country.name.official !== name
);
});
}
}
Thus, like any other signal, we can call update on the ResourceRef instance and filter out the country designated for removal.
This feature shines when combined with template-driven forms. Since signals can now be bound to inputs via [(ngModel)], we can load the data destined for editing and then bind it directly to an input:
@Component({
standalone: true,
template: `
<form>
</input type="email" [(ngModel)]="emailResource" />
</form>
`,
})
export class ChangeEmailComponent {
readonly #userService = inject(UserService);
emailResource = rxResource({
loader: () => this.#userService.getUserEmail(),
});
}
As we can see, we simply bind to the resource itself — clean and straightforward! This can be paired with the newly introduced linkedSignal primitive to craft intricate forms with multiple inputs:
@Component({
standalone: true,
template: `
<form>
<input [(ngModel)]="form.firstName" />
<input [(ngModel)]="form.lastName" />
</input type="email" [(ngModel)]="form.email" />
</form>
`,
})
export class EditUserProfileComponent {
readonly #userService = inject(UserService);
userResource = rxResource({
loader: () => this.#userService.getUser(),
});
form = {
firstName: linkedSignal(() => this.userResource.value()?.firstName ?? ''),
lastName: linkedSignal(() => this.userResource.value()?.lastName ?? ''),
email: linkedSignal(() => this.userResource.value()?.email ?? ''),
};
}
We can now build a form that is both editable by the user and refreshable from HTTP requests when necessary, while keeping the boilerplate to an absolute minimum.
Key considerations
It is worth keeping in mind that both functions are still in developer preview, and their exact behavior may shift before they are finalized.
A notable limitation at the moment is the lack of switch-control. When the source signals change, rxResource/resource will cancel the previous request and start a new one. This default behavior is perfectly fine for most scenarios, but there are cases where you might want to keep multiple requests in flight, or queue them so they run one after another.
With plain RxJS, we could achieve this using operators such as mergeMap or concatMap. Resources, however, do not yet offer that kind of flexibility. It is still an open question whether the Angular team will later introduce options to control the switching strategy.
One more point, and it is crucial: according to the documentation, both rxResource and resource are intended solely for reading data. They are not designed for POST, PUT, or DELETE operations. So avoid using them for mutations, deletions, or form submissions.
Concise API overview
For quick reference, here are tables summarizing the essentials of rxResource and resource (since both return a ResourceRef, a single table is enough for each concern). If you prefer to experiment on your own first, feel free to jump ahead and come back when you need to clarify a detail.
The ResourceStatus enum
This enum lists the possible values that the status signal of a ResourceRef can take.
| ResourceStatus | Description |
|---|---|
Idle |
Status is Idle when either the resource has been destroyed manually, or it has not yet performed its very first request |
Error |
Loading failed with an error. |
Loading |
The resource is currently loading a new value as a result of a change in its request. This status happens when the source signals change, but not when we manually call ResourceRef.reload() |
Reloading |
The resource is currently reloading a fresh value for the same request. This status happens when we manually call ResourceRef.reload() but not when the source signals change |
Resolved |
Loading/Reloading has completed and the resource has the value returned from the loader. |
Local |
The resource's value was set locally via .set() or .update(). Important: this is different from Resolved, so you probably should use it in conjunction if you plan to check for loaded status and also modifying the resource manually |
ResourceRef essentials
| Property | Description | Inherited From |
|---|---|---|
value |
A WritableSignal holding the current value of the resource, or undefined if there is no current value (for instance if the call has not been made yet or it is still loading). Caution: using set or update on this will set the status signal to ResourceStatus.Local |
WritableResource<T> |
status |
A Signal indicating the current status of the resource (e.g., 'Loading', 'Error', 'Resolved'). |
Resource<T> |
error |
A Signal holding the last known error from the resource, if in the Error state. |
Resource<T> |
isLoading |
A Signal indicating whether the resource is currently loading a new value or reloading the existing one. Is true when the status is Loading or Reloading, making it useful for loader checks |
Resource<T> |
hasValue() |
A reactive function that returns true if the resource has a valid current value. |
WritableResource<T> |
reload() |
Instructs the resource to reload its asynchronous dependency. Returns true if a reload was initiated, false otherwise. |
Resource<T> |
set(value) |
Convenience method for setting the value of the resource. Use this instead of ResourceRef.value.set |
WritableResource<T> |
update(updater) |
Convenience method for updating the value of the resource using an updater function. Use this instead of ResourceRef.value.update |
WritableResource<T> |
asReadonly() |
Returns a readonly version of this resource. | WritableResource<T> |
destroy() |
Manually destroys the resource, canceling pending requests and returning it to the idle state. Use this to cancel HTTP calls manually |
ResourceRef<T> |
Final thoughts
Both resource and rxResource are a welcome addition to Angular, and they reinforce the signals pattern that has been steadily maturing over the past year or so.
resource also marks an important move towards reducing Angular's reliance on HttpClient, which remains one of the main ties to RxJS. As the Angular team continues to make RxJS optional, having solid alternatives for common tasks like HTTP requests is essential, and resource is a significant step in that direction.
On top of that, these APIs encourage a more declarative style of programming. Dynamic HTTP requests, in particular, have long been a common source of imperative logic, so having a built-in mechanism that handles them cleanly is a big win.
A quick note

With all the recent changes in Angular, many developers are unsure which approaches to adopt, how to implement them, and what migration paths make sense. I have good news on that front: my first book is about to go to print!
The book is called "Modern Angular", and it covers all the major features introduced between versions 14 and 18, such as standalone components, improved inputs, signals (of course!), better RxJS interoperability, SSR, and plenty more. If that sounds interesting, you can check it out here. It is currently in the copy-editing stage, and all 10 chapters are already available online through Early Access. To stay up to date on the print release, follow me on Twitter or LinkedIn for announcements and special offers.
P.S. If you want to dive deeper into RxJS interoperability, check out chapter 5, and for a closer look at how signals work internally, chapter 7 is the one for you ;)

