With the introduction of signals in Angular v16, the framework started a new path towards rethinking reactivity and change detection. Signals were a welcome and very popular addition. however, everyone understood that they are still in a raw state and more reactive primitives are needed to make them truly useful.

The big difference between signals and RxJS, which is already well-supported in Angular, is that Observables are usually used for async operations (although they can be synchronous too, while signals are always synchronous).

So, naturally, a question of reconciling the two has emerged, and this is how Angular got the resource and rxResource reactive primitives. In this article, we will deconstruct these tools using practical examples, so that you can become comfortable using them in your day-to-day projects.

Note: resource and rxResource are experimental, meaning that the core team does not encourage their use in production ready applications. An RFC from the Angular team is planned to discuss all the outstanding issues. The API of these functions can change drastically in the future. This article will be updated to have links to more up-to-date resources.

The problem

One of the most common tasks in front end development is to fetch some data from a server and then display it in the UI. Previously, we did something like this:

@Component({
    template: `
        <div *ngIf="data">
            <h1>{{data.title}}</h1>
            <p>{{data.description}}</p>
        </div>
    `
})
export class MyComponent implements OnInit {
    readonly #dataService = inject(DataService); 
    data: SomeData = null;

    ngOnInit() {
        this.#dataService.getData().subscribe(data => this.data = data);
    }

}

This more or less covered our concerns, but was verbose and not very flexible. Components that loaded lots of data had lots of disconnected pieces of code, properties declared empty only to be filled in later in subscribe callbacks in ngOnInit or elsewhere.

More savvy developers overcame this problems by using the async pipe, which allows us to read Observable values in the template. Here is the same example rewritten using async:

@Component({
    template: `
        <div *ngIf="data$ | async as data">
            <h1>{{data.title}}</h1>
            <p>{{data.description}}</p>
        </div>
    `
})
export class MyComponent implements OnInit {
    readonly #dataService = inject(DataService); 
    data$ = this.#dataService.getData();
}

This solves the verbosity problem, but still does not address the full scope of concerns related to data fetching. What if I want to show a loading spinner? What about errors? How about retrying, also, what if I don't want to make the request immediately, but rather wait for some value change (like a signal update) or some user event (user clicked on a button)?

Very different approaches have been adopted by Angular developers, some using RxJS operators like switchMap, retry, and some adopting community-based solutions like the derivedAsync function from the ngxtension package.

However, it was high time Angular introduced an own, native tool for solving these concerns.

The solution

In Angular v19, two functions were introduced, resource and rxResource. In essence, they do the same thing, the only difference being that resource works with Promises, while rxResource works with Observables.

As Angular's HTTP client is based on RxJS, let's discuss rxResource and keep in mind that the same things would be true if we used fetch instead of HttpClient.get and resource instead of rxResource.

Loading data

Let's build a working page using rxResource. We will use the Rest Countries API, which is a free API that provides detailed information about countries of the world. Let's begin simple and just build a page that displays all the countries.

export type Country = {
  name: {
    official: string;
  };
  // other properties
};

@Component({
  template: `
    @if (countriesResource.status() === status.Resolved) {
      <ul>
        @for (
          country of countriesResource.value(); 
          track country.name.official
        ) {
          <li>{{ country.name.official }}</li>
        }
      </ul>
    }
  `,
})
export class AppComponent {
  readonly #http = inject(HttpClient);
  status = ResourceStatus;
  countriesResource = rxResource({
    loader: () =>
      this.#http.get<Country[]>('https://restcountries.com/v3.1/all'),
  });
}

Let us break down what goes on here:

  1. With the HttpClient, we fetch the list of countries. This is simple
  2. The rxResource function accepts a configuration object, which contains a loader function.
  3. This loader function is what will be performed to actually make the request. We can put whatever logic we want there, but mainly it would be a call to HttpClient.get.
  4. We also import and use the ResourceStatus enum, which contains the possible states of a resource.
  5. In the template, we first check if the resource is resolved (countriesResource.status() === status.Resolved).
  6. Then, we use the resource to read it's values. The value that we use is actually a signal that will be updated when the resource is resolved.

Now, the status signal contains valuable information about the state of the resource, however, it is not always great for determining if the data is available to display. We will see some differences later in this article, but for now, let us update the example to use another signal called hasValue, which will be true if the resource finished loading and the data is available to read:

@if (countriesResource.hasValue()) {
  <ul>
    @for (
      country of countriesResource.value(); 
      track country.name.official
    ) {
      <li>{{ country.name.official }}</li>
    }
  </ul>
}

This will handle all the outstanding cases. As we can see, everything related to this HTTP request is encapsulated in the countriesResource object. Let's see what else we can do with it.

Handling errors

So, how can we learn if the request failed? And how do we use the actual error message/object? Simple, we can use the error signal and the status again. Here is the updated code:

@if (countriesResource.hasValue()) {
    <ul>
    @for (
        country of countriesResource.value(); 
        track country.name.official
    ) {
        <li>{{ country.name.official }}</li>
    }
    </ul>
} @else if (countriesResource.status() === status.Error) {
    <span>The request failed. Reason: {{ countriesResource.error() }}</span> 
}

As we can see, we only had to update the template to handle the error case. The ResourceRef object produced by rxResource contains all the information we need to know if the request failed, and what the actual error was via the error signal.

Retrying failed requests

What if I want to retry? For example, I might want to show a button that will retry the request.

@if (countriesResource.hasValue()) {
    // omitted for the sake of brevity
} @else if (countriesResource.status() === status.Error) {
    <span>The request failed. Reason: {{ countriesResource.error() }}</span>
    <button (click)="countriesResource.reload()">Retry</button> 
}

The ResourceRef has a reload function, which, in essence, will trigger the same loader function we have provided, allowing us to retry when we have an error, or just reload then request whenever we want.

Loading State

Another common concern for HTTP requests is showing some sort of loading indicator, like a spinner or at least some text. Of course, this is also easily achievable with a resource:

@if (countriesResource.hasValue()) {
    // omitted for the sake of brevity
} @else if (countriesResource.status() === status.Error) {
    <span>The request failed. Reason: {{ countriesResource.error() }}</span>
    <button (click)="countriesResource.reload()">Retry</button> 
} @else if (
    countriesResource.status() === status.Loading ||
    countriesResource.status() === status.Reloading
) {
    <span>Loading Countries...</span>
}

As we can see, there are two distinct statuses here, Loading and Reloading, which indicate whether it is the first time loading the data, or a load triggered later. This can be useful in some scenarios where we want to differentiate between the two.

However, this is a bit cumbersome in generic scenarios, and downright problematic in scenarios where we want to be able to update the resource value manually (later in the article, we will see that this is possible), because in that case the status would be ResourceStatus.Local, indicating that the value has been tampered by some action on the user's end. To avoid the hassle of doing multiple checks, ResourceRef contains a simple signal called isLoading that clearly indicates if there is a necessity to show a loading UI. Let's refactor the code to see it in action:

@if (!countriesResource.isLoading()) {
    // omitted for the sake of brevity
} @else if (countriesResource.status() === status.Error) {
    <span>The request failed. Reason: {{ countriesResource.error() }}</span>
    <button (click)="countriesResource.reload()">Retry</button> 
} @else {
    <span>Loading Countries...</span>
}

Now, before we move on, let's think what would be different if we, for whatever reason, wanted to use completely ditch RxJS and use promises instead. Well, we could not use HttpClient anymore, obviously, as it always returns Observables, and instead of rxResource we would use resource:

export class AppComponent {
  readonly #http = inject(HttpClient);
  status = ResourceStatus;
  countriesResource = resource({
    loader: () => {
      // throw new Error('X');
      return fetch('https://restcountries.com/v3.1/all')
              .then(res => res.json()) as Promise<Country[]>;
    },
  });
}

Very simple change, and, most notably, everything else remains the same. Because of that, and the fact that most Angular codebases use HttpClient (and hence RxJS) for HTTP requests, further down the article we will just use rxResource and you can safely assume that everything we say about it is also true for resource.

Now, let us move on to more complex cases.

Re-evaluating loaded data based on other signals

One very common concern is filtering, sorting or paginating the data fetched from a server. Usually, we have some inputs which the user can interact with to set the page, sorting parameters and so on, and based on those changes, we want to reload the data.

This can easily be achieved with rxResource, as it accepts another configuration parameter, a function that returns the request parameters to be used. Let's imagine we are adding a search input to our page, so we can filter the countries by their name.

@Component({
  template: `
    <input [(ngModel)]="countryName" placeholder="Filter by name"/>
    @if (!countriesResource.isLoading()) {
      <ul>
        @for (
          country of countriesResource.value(); 
          track country.name.official
        ) {
          <li>{{ country.name.official }}</li>
        }
      </ul>
    } @else if (countriesResource.status() === status.Error) {
      <span>The request failed. Reason: {{ countriesResource.error() }}</span>
      <button (click)="countriesResource.reload()">Retry</button> 
    } @else {
      <span>Loading Countries...</span>
    }
  `,
})
export class AppComponent {
  readonly #http = inject(HttpClient);
  countryName = signal('');
  status = ResourceStatus;
  countriesResource = rxResource({
    request: () => ({ name: this.countryName() }),
    loader: (parameters) => {
      return this.#http.get<Country[]>(
        `https://restcountries.com/v3.1/name/${parameters.request.name}`,
      );
    },
  });
}

There are two changes here; first, we added a countryName signal and bound it to the input with [(ngModel)]. Next, we added a request function to the resource configuration object. So what this function does is compute parameters for the request, and if we use signals in this function, it will be re-evaluated whenever the signal changes. The signals in that callback are tracked just as they are tracked in computed of effect.

This means, that we described a whole lifecycle in a simple line of code:

  1. User types the name of the country in the input
  2. The signal is updated via [(ngModel)]
  3. This triggers the re-evaluation of the request function
  4. This triggers the re-evaluation of the loader function, which makes a new HTTP call based on new parameters
  5. This triggers updates to the countriesResource object, which will change its status to Loading (show the loading text/spinner/whatever) and then to Resolved/Error depending on the outcome of the request

All of this happens with us only providing a simple function that describes what reactive parameters our request is dependent on.

Also, if we are using resource instead of rxResource (thus using fetch), the first argument of the request function (which we named parameters in the example) will also contain an abort signal, which we can use to cancel the request if needed!

export class AppComponent {
  readonly #http = inject(HttpClient);
  countryName = signal('');
  status = ResourceStatus;
  countriesResource = resource({
    request: () => ({ name: this.countryName() }),
    loader: (parameters) => {
      // throw new Error('X');
      return fetch(
        `https://restcountries.com/v3.1/name/${parameters.request.name}`,
        {signal: parameters.abortSignal}
      ).then(res => res.json()) as Promise<Country[]>;
    },
  });
}

When we do this, we can then simply use a specific ResourceRef method to cancel the request:

<button (click)="countriesResource.destroy()">Cancel</button>

This abort signal is available both in resource, and rxResource, however, in the case of rxResource it is not necessary for cancellation purposes and can be ignored. With rxResource, we can still call destroy on the ResourceRef object returned by it, in which case it will simply unsubscribe from the Observable that we returned, thus cancelling the request.

Some caveats

However, if we run this exact code (or the previous example with resource) with this specific API URL, we will first encounter an error. This is because the API we are using expects a name, and initially it is an empty string, so the request will fail. So how do we handle this? This is also important in a context where we don't want to make the request immediately, but rather wait for some value change, like this very example.

Well, in our simple case, we can just check if the name parameter has any value, and if not, return an Observable of an empty array:

export class AppComponent {
  readonly #http = inject(HttpClient);
  countryName = signal('');
  status = ResourceStatus;
  countriesResource = rxResource({
    request: () => ({ name: this.countryName() }),
    loader: (parameters) => {
      if (parameters.request.name === '') {
        return of([]);
      }
      return this.#http.get<Country[]>(
        `https://restcountries.com/v3.1/name/${parameters.request.name}`
      );
    },
  });
}

Note: with resource we can use Promise.resolve([]) instead of of([])

Furthermore, if we want to do some specific comparison, the parameters object also contains a previous property, which contains the previous request parameters. This can be useful in scenarios with many source signals, for example, when we want to only reload the data if the user has changed a specific value, but not the others.

Using the data locally

Another great aspect of rxResource/resource is that they contain writable signals, which means we can modify them or bind them to inputs using [(ngModel)]. This is super useful for a very common scenario of loading some data from the server so that the user can then edit it. Here is a short, generic example:

export class SomeComponent {
  readonly #http = inject(HttpClient);
  someValueResource = resource({
    loader: () => this.#http.get<string>('https://some-url.com'),
  });

  updateSomeValue() {
    this.someValue.set('Some new value');
  }
}

As we can see, ResourceRef is a writable signal, and we can use it to update the value of the resource outside of just reloading it from the server.

We can upgrade our example to add logic that allows the user to delete a country by clicking on an "X" button next to it:

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <input [(ngModel)]="countryName" placeholder="Filter by name"/>
    @if (!countriesResource.isLoading()) {
      <ul>
        @for (
          country of countriesResource.value(); 
          track country.name.official;
        ) {
          <li>
            {{ country.name.official }}
            <button (click)="deleteCountry(country.name.official)">X</button>
          </li>
        }
      </ul>
    } @else if (countriesResource.status() === status.Error) {
      <span>The request failed. Reason: {{ countriesResource.error() }}</span>
      <button (click)="countriesResource.reload()">Retry</button> 
    } @else {
      <span>Loading Countries...</span>
    }
  `,
  imports: [FormsModule],
})
export class AppComponent {
  readonly #http = inject(HttpClient);
  countryName = signal('');
  status = ResourceStatus;
  countriesResource = rxResource({
    request: () => ({ name: this.countryName() }),
    loader: (parameters) => {
      if (parameters.request.name === '') {
        return of([]);
      }
      return this.#http.get<Country[]>(
        `https://restcountries.com/v3.1/name/${parameters.request.name}`
      );
    },
  });

  deleteCountry(name: string) {
    this.countriesResource.update((countries) => {
      return countries?.filter(
        (country) => country.name.official !== name
      );
    });
  }
}

So, like any other signal, we can call update on the ResourceRef object and, in this case, filter out the country we want to delete.

However, this is particularly useful in conjunction with template-driven forms. Because it is now possible to bind signals to inputs via [(ngModel)], we can simply load the data that the user will be able to edit, and then bind it directly to some input:

@Component({
  standalone: true,
  template: `
    <form>
      </input type="email" [(ngModel)]="emailResource" />
    </form>
  `,
})
export class ChangeEmailComponent {
  readonly #userService = inject(UserService);
  emailResource = rxResource({
    loader: () => this.#userService.getUserEmail(),
  });
}

As we can see, we simply do a binding to the resource itself - eloquent and simple! This can also be coupled with the new linkedSignal primitive to produce complex forms with multiple inputs:

@Component({
  standalone: true,
  template: `
    <form>
      <input [(ngModel)]="form.firstName" />
      <input [(ngModel)]="form.lastName" />
      </input type="email" [(ngModel)]="form.email" />
    </form>
  `,
})
export class EditUserProfileComponent {
  readonly #userService = inject(UserService);
  userResource = rxResource({
    loader: () => this.#userService.getUser(),
  });

  form = {
    firstName: linkedSignal(() => this.userResource.value()?.firstName ?? ''),
    lastName: linkedSignal(() => this.userResource.value()?.lastName ?? ''),
    email: linkedSignal(() => this.userResource.value()?.email ?? ''),
  };
}

Now, we can create a form that can be both modified by the user and updated from HTTP requests if necessary, all the while keeping the amount of boilerplate to a minimum.

Important to know

First of all, it is important to understand that both functions might yet still evolve, and, as mentioned above, they are still in developer preview.

One significant thing missing right now is switch-control; currently, the requests performed by rxResource/resource will switch from a previous request to a new one when source signals change. This means that previous requests will be canceled to make room for new ones. This is the most common behavior, however sometimes we might want to actually make several requests in parallel, or wait and perform all of them sequentially.

We could easily do that with plain RxJS operators like mergeMap or concatMap, but with resources we are still a bit limited. It remains an open question if the team will later update this API to include choices for switching behaviors.

Finally, and this is very important, as the documentation says, the rxResource and resource functions are only meant to be used for getting data, and not for POST/PUT/DELETE. So, do not use this to edit data, delete items or submit forms.

Simplified API reference

Here I also want to include some tables showing the simplified API of rxResource and resource (both return ResourceRef, so only one table per concern) so you can easily recap what we talked about. Feel free to skip this if you want to try it out yourself, and feel free to get back to it later to clarify nuances.

ResourceStatus enum

This enum contains the possible values of the status signal of a ResourceRef.

ResourceStatus Description
Idle Status is Idle when either the resource has been destroyed manually, or it has not yet performed its very first request
Error Loading failed with an error.
Loading The resource is currently loading a new value as a result of a change in its request. This status happens when the source signals change, but not when we manually call ResourceRef.reload()
Reloading The resource is currently reloading a fresh value for the same request. This status happens when we manually call ResourceRef.reload() but not when the source signals change
Resolved Loading/Reloading has completed and the resource has the value returned from the loader.
Local The resource's value was set locally via .set() or .update(). Important: this is different from Resolved, so you probably should use it in conjunction if you plan to check for loaded status and also modifying the resource manually

ResourceRef simplified API

Property Description Inherited From
value A WritableSignal holding the current value of the resource, or undefined if there is no current value (for instance if the call has not been made yet or it is still loading). Caution: using set or update on this will set the status signal to ResourceStatus.Local WritableResource<T>
status A Signal indicating the current status of the resource (e.g., 'Loading', 'Error', 'Resolved'). Resource<T>
error A Signal holding the last known error from the resource, if in the Error state. Resource<T>
isLoading A Signal indicating whether the resource is currently loading a new value or reloading the existing one. Is true when the status is Loading or Reloading, making it useful for loader checks Resource<T>
hasValue() A reactive function that returns true if the resource has a valid current value. WritableResource<T>
reload() Instructs the resource to reload its asynchronous dependency. Returns true if a reload was initiated, false otherwise. Resource<T>
set(value) Convenience method for setting the value of the resource. Use this instead of ResourceRef.value.set WritableResource<T>
update(updater) Convenience method for updating the value of the resource using an updater function. Use this instead of ResourceRef.value.update WritableResource<T>
asReadonly() Returns a readonly version of this resource. WritableResource<T>
destroy() Manually destroys the resource, canceling pending requests and returning it to the idle state. Use this to cancel HTTP calls manually ResourceRef<T>

In Conclusion

resource/rxResource are a great addition to the Angular ecosystem, and they are definitely expanding on the signals story that has been evolving for more than a year now.

resource is also the first step towards making Angular apps not dependant on HttpClient, which is one of the things that make Angular apps still dependant on RxJS. As Angular team moves in the direction of making RxJS optional, it is important to have tools in place that help replace existing tools that depend on RxJS, and resource is one big step towards that.

Finally, resource/rxResource are great tools to promote declarative programming in Angular apps, and as we all know too well, HTTP requests, especially ones that depend on dynamic values, have been a big source of imperative programming.

Small promotion

Modern Angular.jpeg

The recent upheaval in Angular has caused many developers to be confused about what solutions to chose, how to implement them, and how to migrate their existing codebases to the most recent features. Thankfully, I have a response to this concern: very soon, my very first book is going into print!

It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it here. The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on Twitter or LinkedIn, where I will be posting whenever there are news or promotions available.

P.S. Hey! Check out chapter 5 of my book to learn more about RxJS interoperability and chapter 7 to dive deep into signals and how they work under the hood ;)


Tagged in:

Articles

Last Update: November 20, 2024