This content originally appeared on DEV Community and was authored by Connie Leung
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.
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.
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. Race conditions can occur when we do not pass the AbortSignal in the fetch call. The behavior of the resource function is similar to
switchMap
of RxJS. - The result of the resource function is a Promise.
- If we use HttpClient to return Observables, we can use the rxResource function in the rxjs-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
exhaustMap
of RxJS, and returns the first result. After the loader completes and returns the result, the 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/
This content originally appeared on DEV Community and was authored by Connie Leung