javascriptangulartypescriptrxjsangular-signals

Upto Angular 18 what were the different methods of handling asynchronous API calls, why was the resource API introduced?


I have installed angular 19 and the guide mentions about the new API method called resource.

I want to understand what value it brings to angular with respect to signals.

What was the problems with the signals API to fetch data from observables or promises, I want to understand the significance of resource API.


Solution

  • This can be highlighted using a simple use case, followed by comparing the resource APIs with pre angular 19 methods to achieve the same:

    Motive: I want to reactively call an API based on certain signals, the API is dependant on the signals so it should react.

    TL;DR:

    The resource and rxResource are effective API for making working reactively with asynchronous actions (Promises and Observables) and reduce the dependency on rxjs enabling you to effectively use only signals for these actions.

    Resource API Explanation:

    The advantages:

    The requirement can be achieved by the below easier resource code, rather than the below complex methods:

    TS:

    rs = ResourceStatus;
    id = model(1);
    type = model('posts');
    dataObj: ResourceRef<Todo> = resource({
      request: () => ({
        id: this.id(),
        type: this.type(),
      }),
      loader: ({ request: { id, type } }: any) =>
        fetch(`https://jsonplaceholder.typicode.com/${type}/${id}`).then(
          (response) => {
            return response.json();
          }
        ),
    });
    

    HTML:

    @if(dataObj.status() === rs.Resolved) {
      @let item = dataObj.value();
      <div>{{item?.id}} | {{item?.title}}</div>
    } @else {
      ...Loading...
    }
    <hr/>
    <input [(ngModel)]="id"/>
    <select [(ngModel)]="type">
      <option value="todos">todo</option>
      <option value="posts">posts</option>
    </select>
    

    Stackblitz Demo

    Since we are using fetch, the code is longer that actually achievable through shorter code, using rxResource as shown below.

    TS:

    rs = ResourceStatus;
    http = inject(HttpClient);
    id = model(1);
    type = model('posts');
    dataObj: ResourceRef<Todo> = rxResource({
      request: () => [this.id(), this.type()],
      loader: ({ request: [id, type] }: any) =>
        this.http.get<Todo>(`https://jsonplaceholder.typicode.com/${type}/${id}`),
    });
    

    The html is the same as above but smaller.

    Stackblitz Demo


    Before the dawn of resource and rxResource, below were the method of fetching data from API using signals.

    Pre Resource and RxResource methods:

    Problems: Too much code | multiple imports | not scalable | not maintainable | highly dependent on rxjs.

    Method 1: Usage of rxjs interop function - toObservable to convert the signal to an observable to achieve reactivity:

    Explanation: We use toObservable to convert the signals to observables, then we use [combineLatest][combineLatest] from rxjs to accumulate the signals together, then use [switchMap][switchMap] to trigger the API call from the signal-observables we created, then using async pipe, we can access this data and react to the changes.

    TS:

    http = inject(HttpClient);
    id = model(1);
    type = model('posts');
    // use toObservable to convert the signal to an observable
    dataObj: Observable<Todo> = combineLatest([
      toObservable(this.id),
      toObservable(this.type),
    ]).pipe(
      // make the API call using switchMap
      switchMap(
        ([id, type]: [number, string]): Observable<Todo> =>
          this.http.get<Todo>(
            `https://jsonplaceholder.typicode.com/${type}/${id}`
          )
      )
    );
    

    HTML:

    @if(dataObj | async; as item) {
      <div>{{item.id}} | {{item.title}}</div>
    }
    <input [(ngModel)]="id"/>
    <select [(ngModel)]="type">
      <option value="todos">todo</option>
      <option value="posts">posts</option>
    </select>
    

    Stackblitz Demo


    Method 2: Usage of rxjs interop function - toSignal to convert the signal to observables (which is used to make the API) then to a signal to achieve reactivity:

    Explanation: We use toObservable to convert the signals to observables, then we use [combineLatest][combineLatest] from rxjs to accumulate the signals together, then use [switchMap][switchMap] to trigger the API call from the signal-observables we created, then using toSignal we get rid of the async pipe, or chain this to a computed to perform calculations, we can access this data and react to the changes.

    TS:

    http = inject(HttpClient);
    id = model(1);
    type = model('posts');
    // use toObservable to convert the signal to an observable
    dataObj: Signal<Todo | undefined> = toSignal(
      combineLatest([toObservable(this.id), toObservable(this.type)]).pipe(
        // make the API call using switchMap
        switchMap(
          ([id, type]: [number, string]): Observable<Todo> =>
            this.http.get<Todo>(
              `https://jsonplaceholder.typicode.com/${type}/${id}`
            )
        )
      )
    );
    

    HTML:

    @if(dataObj(); as item) {
      <div>{{item.id}} | {{item.title}}</div>
    }
    <input [(ngModel)]="id"/>
    <select [(ngModel)]="type">
      <option value="todos">todo</option>
      <option value="posts">posts</option>
    </select>
    

    Stackblitz Demo


    Method 3: Using computed to achieve reactivity along with async pipe:

    Explanation: We use computed which reacts when the signals inside change. The computed will return an observable which we can use with async pipe to access the data. The problem with this approach is that the data in HTML get's rerendered when the observables change, so bad for performance.

    TS:

    http = inject(HttpClient);
    id = model(1);
    type = model('posts');
    // use computed to react to the signals and create an observable, that has the latest data.
    // the problem with this approach is that the data inside the if gets recreated on each emit
    dataObj: Signal<Observable<Todo | undefined>> = computed(() => {
      return this.http.get<Todo>(
        `https://jsonplaceholder.typicode.com/${this.type()}/${this.id()}`
      );
    });
    

    HTML:

    @if(dataObj() | async; as item) {
      <div>{{item.id}} | {{item.title}}</div>
    }
    <hr/>
    <input [(ngModel)]="id"/>
    <select [(ngModel)]="type">
      <option value="todos">todo</option>
      <option value="posts">posts</option>
    </select>
    

    Stackblitz Demo

    Apart from these there are several other methods using effect also.