Angular team releases experimental resource
and rxResource
functions in Angular version 19 to facilitate data retrieval. The functions come with two favors: resource
produces a Promise, and rxResource
produces an Observable. If applications use HttpClient to return an Observable, engineers can refactor the codes with the rxResource
function in the rxjs-interop
package.
I have an old Angular 16 repository that uses the HttpClient to make HTTP requests to the server to retrieve a Pokemon. Link: https://github.com/railsstudent/ng-pokemon-signal/tree/main/projects/pokemon-signal-demo-10/. I will rewrite the project in 19.0.0-next.0 in this blog post and apply both functions to retrieve data.
Demo 1: Retrieve the Pokemon data by the resource function
Implement an adapter function
// pokemon.adapter.ts
import { Ability, DisplayPokemon, Pokemon, Statistics } from './interfaces/pokemon.interface';
export const pokemonAdapter = (pokemon: Pokemon): DisplayPokemon => {
const { id, name, height, weight, sprites, abilities: a, stats: statistics } = pokemon;
const abilities: Ability[] = a.map(({ ability, is_hidden }) => ({
name: ability.name, isHidden: is_hidden }));
const stats: Statistics[] = statistics.map(({ stat, effort, base_stat }) => ({ name: stat.name, effort, baseStat: base_stat }));
return {
id,
name,
… other properties
}
}
The pokemonAdapter
function changes the HTTP response into the shape that the components expect to display the properties of a Pokemon in the view.
Implement a service to define the Pokemon resource
// pokemon.service.ts
import { Injectable, resource, signal } from '@angular/core';
import { DisplayPokemon, Pokemon } from '../interfaces/pokemon.interface';
import { pokemonAdapter } from '../pokemon.adapter';
@Injectable({
providedIn: 'root'
})
export class PokemonService {
private readonly pokemonId = signal(1);
readonly pokemonResource = resource<DisplayPokemon, number>({
request: () => this.pokemonId(),
loader: async ({ request: id, abortSignal }) => {
try {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`, { signal: abortSignal });
const result = await response.json() as Pokemon
return pokemonAdapter(result);
} catch (e) {
console.error(e);
throw e;
}
}
});
updatePokemonId(value: number) {
this.pokemonId.set(value);
}
}
The PokemonService
service uses the resource
function to retrieve the Pokemon by an ID. The resource
options have four properties: request, loader, equal, and injector, which should look familiar to developers using Angular Signal. I only used request
and loader
in this example; the request
option is a function that tracks the pokemonId
signal. When the signal is updated, the loader executes the fetch
call to retrieve a Pokemon by an ID and resolve the Promise to obtain the response. Then, the pokemonAdapter
function transforms the response before returning the final result to the components. The resource
function runs the loader function inside untracked; therefore, any signal change in the loader does not cause any computation and rerun.
The parameter of the loader function is of type ResourceLoaderParams
. It has the following properties:
- request: the result of the request function
- abortSignal: an instance of AbortSignal
- previous: an Object that holds the previous status
If we want to cancel the previous running requests, we will pass abortSignal
to the fetch call. If we unuse abortSignal
, the resource
function will discard the result of requests canceled by the subsequent one. The resource's behavior is similar to the switchMap
operator of RxJS.
Design the shared Pokemon View
// pokemon.component.html
<h2>{{ title }}</h2>
<div>
@let resource = pokemon.value();
@let hasValue = pokemon.hasValue();
@let isLoading = pokemon.isLoading();
<p>Has Value: {{ hasValue }}</p>
<p>Status: {{ pokemon.status() }}. Status Enum: 0 - Idle, 1 - Error, 2 - Loading, 4 is Resolved.</p>
<p>Is loading: {{ isLoading }}</p>
<p>Error: {{ pokemon.error() }}</p>
@if (isLoading) {
<p>Loading the pokemon....</p>
} @else if (resource) {
<div class="container">
<img [src]="resource.frontShiny" />
<img [src]="resource.backShiny" />
</div>
<app-pokemon-personal [pokemon]="resource"></app-pokemon-personal>
<app-pokemon-tab [pokemon]="resource"></app-pokemon-tab>
<app-pokemon-controls [(search)]="pokemonId"></app-pokemon-controls>
}
</div>
This is the shared template of the PokemonComponent
and RxPokemonComponent
components. The return type of the resource
function is ResourceRef
that has the following signal properties:
- value: the resource’s data
- hasValue: whether or not the resource has value
- isLoading: whether or not the status is loading
- status: the status of the resource. 0 is Idle, 1 is error, 2 is Loading, 3 is Reloading, 4 is Resolved and 5 is Local. error: the error of the resource when the loader function throws it.
ResourceRef
extends WritableResource
; it exposes set()
and update()
to overwrite the resource. When it occurs, the status becomes Local.
Glue everything together in the Pokemon component
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, filter, map, Observable } from "rxjs";
import { POKEMON_MAX, POKEMON_MIN } from '../constants/pokemon.constant';
export const searchInput = (minPokemonId = POKEMON_MIN, maxPokemonId = POKEMON_MAX) => {
return (source: Observable<number>) => source.pipe(
debounceTime(300),
filter((value) => value >= minPokemonId && value <= maxPokemonId),
map((value) => Math.floor(value)),
distinctUntilChanged(),
takeUntilDestroyed()
);
}
// pokemon.component.ts
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [PokemonControlsComponent, PokemonPersonalComponent, PokemonTabComponent],
templateUrl: './pokemon.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class PokemonComponent {
private readonly pokemonService = inject(PokemonService);
pokemonId = signal(1);
pokemon = this.pokemonService.pokemonResource;
constructor() {
toObservable(this.pokemonId).pipe(searchInput())
.subscribe((value) => this.pokemonService.updatePokemonId(value));
}
}
The pokemonId
signal is two-way binding to the search
model input of the PokemonControlsComponent
component. When it updates, the toObservable
emits the value to the custom RxJS operator to debounce 300 milliseconds before setting the pokemonId
signal in the service. It causes the loader function to call the backend to retrieve the new data and update the view.
Demo 2: Retrieve the Pokemon data by the rxResource function
Implement a service to define the Pokemon resource
// rx-pokemon.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { catchError, delay, map, of } from 'rxjs';
import { DisplayPokemon, Pokemon } from '../interfaces/pokemon.interface';
import { pokemonAdapter } from '../pokemon.adapter';
@Injectable({
providedIn: 'root'
})
export class RxPokemonService {
private readonly httpClient = inject(HttpClient);
private readonly pokemonId = signal(1);
readonly pokemonRxResource = rxResource<DisplayPokemon | undefined, number>({
request: () => this.pokemonId(),
loader: ({ request: id }) => {
return this.httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
.pipe(
delay(500),
map((pokemon) => pokemonAdapter(pokemon)),
catchError((e) => {
console.error(e);
return of(undefined);
})
);
}
});
updatePokemonId(input: number) {
this.pokemonId.set(input);
}
}
This service is similar to the PokemonService service, except the pokemonRxResource
member calls the rxResource
function to obtain an Observable. Similarly, the request option tracks the pokemonId
signal. The loader function uses the HttpClient
to request an HTTP GET to retrieve a Pokemon by an ID.
The rxResource
function behaves the same as firstValueFrom
; only the first emission of the Observable is considered. The loader function emits the value to take(1)
that takes the first emission or is canceled by the cancel Subject via takeUntil
.
Glue everything together in the Pokemon component
// rx-pokemon.component.ts
@Component({
selector: 'app-rx-pokemon',
standalone: true,
imports: [PokemonControlsComponent, PokemonPersonalComponent, PokemonTabComponent],
templateUrl: './pokemon.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class RxPokemonComponent {
private readonly pokemonService = inject(RxPokemonService);
pokemon = this.pokemonService.pokemonRxResource;
pokemonId = signal(1)
constructor() {
toObservable(this.pokemonId).pipe(searchInput())
.subscribe((value) => this.pokemonService.updatePokemonId(value));
}
}
The pokemonId
signal is two-way binding to the search model input of the PokemonControlsComponent. When it updates, the toObservable
emits the value to the custom RxJS operator to debounce 300 milliseconds before setting the pokemonId
signal in the service. It causes the loader function to call the backend to retrieve the new data and update the view.
Conclusions:
- The resource function listens to the request and makes an HTTP request to retrieve a Pokemon by an ID.
- The loader input consists of the request value and an instance of AbortSignal. The AbortSignal is used to cancel the previous requests that are still running. If we unuse the AbortSignal, the resource function discards the result of canceled requests. The behavior of the resource function is similar to the
switchMap
operator of RxJS. - The result of the loader function is a Promise for both the
resource
andrxResource
functions. - If we use HttpClient to return Observables, we can use the
rxResource
function in therxjs-interop
package. - The
rxResource
function uses AbortSignal under the hood; therefore, it does not pass the signal into the HttpClient. - The behavior of rxResource is similar to the
firstValueFrom
operation of RxJS, and returns the first emission of the Observable stream. After the loader completes, therxResource
function can accept new requests from components.
References:
- Resource API PRs:
- Pokemon Resource Github Repo: https://github.com/railsstudent/ng-pokemon-resource
- Old Pokemon Github Repo: https://github.com/railsstudent/ng-pokemon-signal/tree/main/projects/pokemon-signal-demo-10/
Top comments (3)
Great article! I’m really looking forward to this new API. I believe it’s going to make our code a lot cleaner. I love the idea of having the loading state automatically managed by a core feature of Angular since I’ve seen this go wrong so often when developers handle it themselves. I’ll probably still use Angular Query most of the time for all its extra features, but I think it’s fantastic to have an elegant, signal-based API that manages loading state in Angular core. I’m curious to see what comes from the RFC!
Out of curiosity, why would you use
fetch
instead of the Angular HTTP client? I’m not sure if I like needing to manually specify an abort signal and convert the response to JSON—not to mention losing access to Angular's HTTP interceptors—it seems like a step backwards to me.The Angular team released resource and resource together, and I tried both to know how they work before writing this blog post. I prefer rxResource that allows me to emit the Observable to RxJS operators to transform the stream. For example, delay() simulates a long request that see the changes of isLoading() and status().
Either function is missing debounce in my opinion. When rewriting this demo, they started an HTTP request immediately after I inputted the first digit in the input field. After consulting other Angular GDEs, I resorted to toObservable to pipe the ID to debounceTime to give me extra time to input more digits in the input field before calling the Pokemon API.
Hi Connie Leung,
Thanks for sharing.