DEV Community

Connie Leung
Connie Leung

Posted on

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

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
    );
}
Enter fullscreen mode Exit fullscreen mode
//  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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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); 
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
  }
} 
Enter fullscreen mode Exit fullscreen mode

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 and rxResource functions.
  • 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 the firstValueFrom operation of RxJS, and returns the first emission of the Observable stream. After the loader completes, the rxResource function can accept new requests from components.

References:

Top comments (3)

Collapse
 
rensjaspers profile image
Rens Jaspers • Edited

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.

Collapse
 
railsstudent profile image
Connie Leung • Edited

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.

Collapse
 
jangelodev profile image
João Angelo

Hi Connie Leung,
Thanks for sharing.