Data retrieval with the experimental resource and rxResource functions in Angular 19



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:


This content originally appeared on DEV Community and was authored by Connie Leung