DEV Community

Amos Isaila
Amos Isaila

Posted on • Originally published at codigotipado.com on

Angular 19: resource() API for async dependencies

Angular 19 introduces a powerful new reactive primitive called resource() that helps manage asynchronous dependencies through the signal system. Resources are particularly useful for handling API call

Core Concepts

A Resource consists of:

  • A reactive request function that determines what to fetch
  • An async loader function that handles the actual fetching
  • Signals exposing the current state and value
  • Built-in request cancellation and cleanup

Resource States

Resources can be in one of these states (ResourceStatus enum):

  • Idle: no valid request, no loading.
  • Loading: initial load for a request.
  • Reloading: loading fresh data for same request.
  • Resolved: successfully loaded.
  • Error: loading failed.
  • Local: value was set manually.

Original vs Resource-based Implementation

I have a cats facts app build with Signals and we are going to refactor the request to be able to use the resource() API.

First, let’s look at how we can refactor the CatsFactsService:

// cats-facts.service.ts

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { catchError, map, Observable } from 'rxjs';

export interface CatFactResponse {
  data: string[];
}

@Injectable({ providedIn: 'root' })
export class CatsFactsService {
  private readonly _apiUrl = 'https://meowfacts.herokuapp.com';
  private readonly _http = inject(HttpClient);

  // We have to refactor this
  getCatsFacts(count = 10): Observable<string[]> {
    return this._http
      .get<CatFactResponse>(`${this._apiUrl}`, {
        params: { count: count.toString() },
      })
      .pipe(
        map((response) => response.data),
        catchError(this.handleError)
      );
  }

  private handleError(error: any): Observable<never> {
    console.error('An error occurred:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

So, let’s refactor it using the new resource API:

readonly getCatsFacts = resource({
    loader: async () => {
      try {
        const response = await (await fetch(`${this._apiUrl}/?count=10`)).json() as { data: string[] };
        return response.data;
      } catch(error) {
        throw error;
      }
    }
  });
Enter fullscreen mode Exit fullscreen mode

As you can see the resource() has a loader parameter that returns a Promise.

Consume resource()

In our controller we can consume it this way:

@Component({
  selector: 'app-cat-facts',
  templateUrl: './cats-facts.component.html',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CatFactsComponent {
  private readonly _catFactsService = inject(CatsFactsService);
  factsResource = this._catFactsService.getCatsFacts;
}
Enter fullscreen mode Exit fullscreen mode

And in our template we can use the new @let syntax to reference all the values from the resource:

@let facts = factsResource.value() ?? [];
@let hasValue = factsResource.hasValue();
@let status = factsResource.status();
@let isLoading = factsResource.isLoading();
@let error = factsResource.error();
Enter fullscreen mode Exit fullscreen mode

Then you can loop the facts and print the values. In the picture you can see that with little card im referencing those values:

I have a pipe that resolve the resource() status:

@Pipe({
  name: 'resourceStatus',
  standalone: true,
})
export class ResourceStatusPipe implements PipeTransform {
  transform(status: ResourceStatus): string {
    switch (status) {
      case ResourceStatus.Idle:
        return 'Idle';
      case ResourceStatus.Error:
        return 'Error';
      case ResourceStatus.Loading:
        return 'Loading';
      case ResourceStatus.Resolved:
        return 'Resolved';
      case ResourceStatus.Reloading:
        return 'Reloading';
      case ResourceStatus.Local:
        return 'Local';
      default:
        return 'Unknown';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If I print with a console.log the status path it will go from 2 (Loading) to 4 (Resolved) in our case.

Load more button (update locally)

You can upadte you resource locally this way:

loadMore(): void {
  this.factsResource.value.update((values: string[] | undefined) => {
    if (!values) {
      return undefined;
    }
    return [...values, 'Other fact!'];
  });
  this.count.update((ct) => (ct += 5));
}
Enter fullscreen mode Exit fullscreen mode

The update method adds one more item to our list, and our state changes to 5 (ResourceStatus.Local). Note that you can also use the set method to replace the facts with a new values.

Restarting our facts (refreshing)

To be able to restart our request we just have to invoke the reload method:

restartFacts(): void {
  this.factsResource.reload();
}
Enter fullscreen mode Exit fullscreen mode

And the status will change from 3 (Reloading) to 4 (Resolved).

Note that if you click multiple times on the Restart button, the loader function will only be called once, just as the switchMap operator from RxJS works.

Load 10, 20 30… facts

How about if you want to load more facts based on some signal? Let’s look at this:

@Injectable({ providedIn: 'root' })
export class CatsFactsService {
  private readonly _apiUrl = 'https://meowfacts.herokuapp.com';
  private readonly count = signal(10);

  readonly getCatsFacts = resource({
    loader: async () => {
      try {
        const response = await (await fetch(`${this._apiUrl}/?count=${this.count()}`)).json() as { data: string[] };
        return response.data;
      } catch(error) {
        throw error;
      }
    }
  });

  updateCount(value: number): void {
    this.count.set(value);
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see in the loader function, we added the count signal. But this won’t trigger the request again if we change the count. This is because the loder function is untracked.

So, to be able to call again the request, we need to use the request parameter from the resource object:

readonly getCatsFacts = resource({
    request: this.count,
    loader: async ({ request: count }) => {
      try {
        const response = await (await fetch(`${this._apiUrl}/?count=${count}`)).json() as { data: string[] };
        return response.data;
      } catch(error) {
        throw error;
      }
    }
  });
Enter fullscreen mode Exit fullscreen mode

Now everytime we change our signal, the request will be triggered again!

AbortSignal

In the previous example we load the request again and again based on some signal change. But what if we change multiple times the signal, won’t be nice to cancel previous requests? We can do that by passing the abortSignal to the loader function:

readonly getCatsFacts = resource({
    request: this.count,
    loader: async ({ request: count, abortSignal }) => {
      try {
        const response = await (await fetch(`${this._apiUrl}/?count=${count}`, { signal: abortSignal })).json() as { data: string[] };
        return response.data;
      } catch(error) {
        throw error;
      }
    }
  });
Enter fullscreen mode Exit fullscreen mode

With this, if the previous request is still loading and we have a new one, the previous one will be canceled.

RxResource

The Angular team also introduced an Observable approach. So you can use Observables in your request. So, the loader function will return an Observable instead of a Promise. Let’s see it in action by refactoring our service:

import { HttpClient } from '@angular/common/http';
import { inject, Injectable, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';

export interface CatFactResponse {
  data: string[];
}

@Injectable({ providedIn: 'root' })
export class CatsFactsRxResourceService {
  private readonly _http = inject(HttpClient);
  private readonly _apiUrl = 'https://meowfacts.herokuapp.com';
  private readonly count = signal(10);

  readonly getCatsFacts = rxResource({
    request: this.count,
    loader: (count) => {
      return this._http
      .get<CatFactResponse>(`${this._apiUrl}`, {
        params: { count: count.toString() },
      })
      .pipe(
        map((response) => response.data),
        catchError(this.handleError)
      );
    }
  });

  updateCount(value: number): void {
    this.count.set(value);
  }

  private handleError(error: any): Observable<never> {
    console.error('An error occurred:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

The same behaviour will happen, you can cancel previous requests if new ones are triggered and also change local state.

Conclusions

It’s a powerful API that need more feedback from the Angular community. Actually it’s experimental so try to play with it and make your own shapes. This adds more reactivity to our requests.

And as Alex from the Angular team said, there will be a Resource RFC.

This feature will be landed as experimental in Angular 19.

Important resources

Thanks for reading so far 🙏

I’d like to have your feedback so please leave a comment , clap or follow. 👏

Spread the Angular love! 💜

If you really liked it, share it among your community, tech bros and whoever you want! 🚀👥

Don’t forget to follow me and stay updated: 📱

Thanks for being part of this Angular journey! 👋😁

Originally published at https://www.codigotipado.com.

Top comments (0)