Table of Contents
Introduction
Resource API has some changes in Angular 20.
-
requestwill be renamed toparams -
loaderis renamed tostreamin rxResource. Therefore,streamwill be used for streaming and querying a resource. - ResourceStatus is a string union type.
In this blog post, I will show an example of using rxResource to stream Pokemons and append them to a list.
The example creates three rxResource to query and stream the Pokemons. When users click the "Load More..." button, the URL updates. The first rxResource retrieves the pokemon URLs of the specific page. The second rxResource uses the mergeMap operator to retrieve the Pokemons concurrently. When the Pokemons arrive successfully, the RxJS operator, reduce, appends them to a list. The last rxResource streams the new data and the new Pokemons are shown at the end of the page.
Define a PokemonPageService
- Create a Pokemon Page Service
First, I define a PokemonPageService with a couple methods. One method pageinates the Pokemon URLS and another method retrieve the Pokemons concurrently.
The paginate method makes a request to the Pokemon API to retrieve an array of Pokemon URL.
@Injectable({
providedIn: 'root'
})
export class PokemonPageService {
private readonly http = inject(HttpClient);
paginate(url: string) {
return this.http.get<PokemonPageUrlType>(url);
}
}
The concurrentPokemons method retrieves at most three Pokemons concurrently. In the demo, the pagination retrieves five Pokemons. Three Pokemons are retrieved in parallel and returned. When the requests completed, the next two Pokemons are retrieved. The behavior is different than forkJoin that waits for all the Observables to complete before returning all the Pokemons. The user experience is better because users can see some results sooner.
@Injectable({
providedIn: 'root'
})
export class PokemonPageService {
private readonly http = inject(HttpClient);
private retrievePokemonByUrl(url: string) {
return this.http.get<PrePokemon>(url)
}
concurrentPokemons(urls: string[], concurrent=3) {
if (!urls || urls.length <=0) {
return of(undefined);
}
// The delay is added to illustrate that the next two Pokemons are retrieved when the previous batch request ends.
return from(urls).pipe(
mergeMap((url) => this.retrievePokemonByUrl(url), concurrent),
delay(800),
reduce((acc, pokemon) => pokemon ? acc.concat(pokemon) : acc,
[] as Pokemon[]),
map((pokemons) => pokemons.sort((a, b) => a.id - b.id)),
);
}
}
- Construct rxResource to Query Pokemons
The PokemonPageComponent creates a rxResource that calls the PokemonPageService whenever the url signal is updated. The request property is renamed to params and it is a reactive function that returns the value of the url signal. The loader property is renamed to stream. The function destructures the parameter and renames params to url. The url is passed to the service to obtain the Pokemons to display.
url = signal('https://pokeapi.co/api/v2/pokemon?limit=5');
pokemonPageService = inject(PokemonPageService);
// When URL updates, the rxResource retrieves a page of Pokemons
pokemonPageResource = rxResource<PokemonPageUrlType, string>({
params: () => this.url(),
stream: ({ params: url }) => this.pokemonPageService.paginate(url),
defaultValue: { ... default values ... }
});
- The
urlsignal is updated when users click the "Load More" button.
<div class="load">
<button (click)="loadNext()">Load More...</button>
</div>
After the pokemonPageResource has loaded a new page, the concurrentPokemonResource rxResource collects all the Pokemon URLs and pass them to the PokemonPageService to retrieve the Pokemons concurrently.
value = computed(() =>
this.pokemonPageResource.hasValue() ?
this.pokemonPageResource.value() : { results: [] as PokemonUrl[], next: '' }
);
concurrentPokemonsResource = rxResource({
params: () => this.value(),
stream: ({ params }) => {
const urls = params.results.map((r) => r.url);
return this.pokemonPageService.concurrentPokemons(urls)
},
defaultValue: [] as Pokemon[]
});
pokemons = computed(() => this.concurrentPokemonsResource.hasValue() ?
this.concurrentPokemonsResource.value() : [] as Pokemon[]);
The pokemons is also the signal input of the PokemonListComponent.
<app-pokemon-list [pokemons]="pokemons()" />
Next, I will show how to use rxResource to stream the Pokemons.
Stream Pokemons with rxResource
@Component({
selector: 'app-pokemon-list',
imports: [PokemonRowComponent],
templateUrl: './pokemon-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonListComponent {
pokemons = input.required<Pokemon[]>();
pokemonList$ = toObservable(this.pokemons)
.pipe(
scan((acc, newPokemons) => newPokemons ? acc.concat(newPokemons) : acc,
[] as Pokemon[]),
);
pokemonStreamResource = rxResource({
stream: () => this.pokemonList$
});
pokemonList = computed(() => this.pokemonStreamResource.hasValue() ?
this.pokemonStreamResource.value() : []
);
}
The pokemons is a signal input of the Pokemons.
ThepokemonList$ is an Observable that appends the new Pokemons to the Pokemon list. Moreover, the stream property of rxResource expects a function that returns an Observable.
The pokemonStreamResource streams the pokemonList$ Observable and displays the new data in the HTML template.
@if (pokemonList()) {
<h2>Pokemon List ({{ pokemonList().length }})</h2>
@for (result of pokemonList(); track result.name) {
<app-pokemon-row [result]="result" />
<hr />
} @empty {
<p>No Pokemon</p>
}
}
This is how we can use rxResource and the Signal API to stream Pokemons in Angular 20.
Demos
Resources
- The PR relevant to the Resource API: PR60919
- Resource Doc: https://next.angular.dev/guide/signals/resource#
- resource API: https://next.angular.dev/api/core/resource#
- rxResource API: https://next.angular.dev/api/core/rxjs-interop/rxResource#
- Concurrent Requests
Top comments (0)