Asynchronous Data Flow with Angular’s new Resource API

  1. Signals in Angular: The Future of Change Detection
  2. Component Communication with Signals: Inputs, Two-Way Bindings, and Content/ View Queries
  3. Successful with Signals in Angular – 3 Effective Rules for Your Architecture
  4. Skillfully Using Signals in Angular – Selected Hints for Professional Use
  5. When (Not) to use Effects in Angular — and what to do instead
  6. Asynchronous Data Flow with Angular’s new Resource API

About a month before the release of version 19, Angular's Alex Rickabaugh published a PR for a new reactive API. The so-called Resource API allows for asynchronously loading resources using Signals. A typical use case for resource is loading data via HTTP.

In this article, I show how to build a typical CRUD scenario using the new resource API. As it has not been released at the time of writing, my examples directly use the code from the PR. I will update this article when resource lands in Angular to reflect possible updates and changes.

📂 Source Code (see Branch 07c-final)

Updates:

  • 2024-11-01: Update for Angular 19 RC 0

Example Application

To demonstrate the individual features, I'm using the desserts application already known from former articles:

Example Application: Austrian Desserts

It basically allows searching for Austrian desserts using the original Austrian German name or its English translation. You can rate the individual desserts or load the ratings provided by a recognized expert in this field (who happens to be my lesser self).

Also, there is a details view for editing a dessert:

First Steps with the Resource API

Each resource has a loader function that returns a Promise with the loaded data. This loader is triggered when the resource has been initialized. Optionally, the resource can have a request Signal providing parameters (search criteria) for the loader. Every time the request Signal changes, the loader is triggered again:

@Component([...])
export class DessertsComponent {
  #dessertService = inject(DessertService);

  [...]

  // Criteria for search
  originalName = signal('');
  englishName = signal('');

  // Combine criteria to computed Signal
  dessertsCriteria = computed(() => ({
    originalName: this.originalName(),
    englishName: this.englishName(),
  }));

  // Define resource with request (=search criteria) and loader
  // Every time, the request (criteria) is changing, the loader is triggered
  dessertsResource = resource({
    request: this.dessertsCriteria,
    loader: (param) => {
      return this.#dessertService.findPromise(param.request);
    }
  });

  // initially, resources are undefined
  desserts = computed(() => this.dessertsResource.value() ?? []);

  loading = this.dessertsResource.isLoading;
  error = this.dessertsResource.error;

  // The reactive flow goes on ...
  ratings = signal<DessertIdToRatingMap>({});
  ratedDesserts = computed(() => this.toRated(this.desserts(), this.ratings()));

  [...]
}

In this example, dessertsResource uses the values in the originalName and englishName Signals as parameters. The loader is triggered initially and when these Signals change. The param object passed to the loader contains the current search criteria from the request Signal.

The resource's result is found in its value Signal. The computed desserts Signal switches out the initial undefined value by an empty array. The boolean isLoaded informs about the loading state, and error is a Signal with a possible error that occurred during loading.

The computed ratedDesserts Signal combines the received desserts with possibly loaded ratings. This shows that we have a clear reactive flow that extends from the user input through loading the resource to projecting the resource to a model bound in the view. One action reactively leads to another.

Important: Loaders are Untracked!

It's important to note that while the request is tracked, the loader isn't. That means that a change in the request Signal triggers a new loader execution, but a change in a Signal used in the loader does not.

This is because auto-tracking would only work in the first part of the loader that is executed synchronously. Everything that is executed after the first asynchronous operation, e.g., code after await or in a .then handler, cannot be subject to Angular's auto-tracking. To avoid such confusing situations where a part of the loader is handled differently, the whole loader is untracked.

Race Conditions

In many web applications, the user can easily trigger overlapping requests. This is especially the case in an reactive UI where just changing a filter can trigger a further loading operation:

Overlapping Requests

In this case, the user would expect to only get results for 'Ice Cream Pancakes' although they shortly had different filters before. It would be quite confusing if results for ordinary 'Pancakes', and 'Sacher Cake' briefly flashed. It would be even more confusing if the first search took a bit longer:

Overlapping Requests

In this case, the unwanted intermediate results would flash in an order that does not match the order of requests. You call this a race condition.

The Resource API has switchMap Semantics

In Angular, we usually leverage RxJS and its switchMap Operator that cancels overlapping requests but the last one and hence prevents such situations. The good news is that resource uses the same behavior. However, by default it cannot cancel the former request. Instead, it just ignores its result.

If you really want to cancel the former request when another one is scheduled, you need to respect the AbortSignal the resource API passes into the loader:

[...]
dessertsResource = resource({
  request: this.dessertsCriteria,
  loader: (param) => {
    return this.#dessertService.findPromise(
      param.request,
      param.abortSignal,
    );
  },
});
[...]

Although it's called AbortSignal, it's not a Signal in the sense of Angular's reactivity primitives but a JavaScript API provided by all modern browsers and supported by several APIs such as the Fetch API. While Angular's HttpClient is meanwhile capable of using fetch under the hoods, its API does not allow passing in an AbortSignal. To compensate for this, I've written a simple helper function toPromise that converts an Observable to a Promise respecting an AbortSignal. You find this implementation that is inspired by the rxResource function mentioned below, in the provided demo repository.

While switchMap semantics might be the most common one used for loading data, RxJS provides further so-called flattening operators dealing with overlapping requests in different ways. However, for the discussed operations, resource always uses switchMap semantics. Besides this, the reload method discussed below always uses exhaustMap sematics.

This shows that the Angular team does not want to compete with existing libraries. The goal is to provide building blocks with reasonable default logic covering the majority of use cases. For everything that goes beyond that, we can implement our own solutions or add proven libraries like RxJS.

Debouncing

Usually, when triggering requests directly after the user changes a filter, we want to have some debouncing: Chances are, the user is changing further filters, and hence, we want to wait some moments to not trigger several unnecessary requests in a row.

As the resource loader is returning a Promise that is aborted (or ignored) when a subsequent overlapping request comes in, we just need to add a Promise-based version of setTimeout:

dessertsResource = resource({
  request: this.dessertsCriteria,
  loader: async (param) => {

    // Debouncing: 300 ms
    await wait(300, param.abortSignal);

    return await this.#dessertService.findPromise(
      param.request,
      param.abortSignal,
    );
  },
});

Please note that I switched to using await and, hence, to async to ensure that the resource is loaded after the debouncing. The function wait is a Promise-based wrapper for setTimeout respecting the passed AbortSignal. You find its implementation in the provided repository.

More on this: Angular Architecture Workshop (online, interactive, advanced)

Become an expert for enterprise-scale and maintainable Angular applications with our Angular Architecture workshop!

All Details (English Workshop) | All Details (German Workshop)

Reload and Manual Loading

The resource used so far automatically triggered the loader after its initialization and after each change of the request Signal. This was convenient in the example at hand. However, it's not a behavior you want all the time.

If you want to disable the initial loader execution, your loader can check if the resource is in idle state. This is the initial state and also the state it transitions to when it is destroyed. Alternatively, you can also use a class property with a flag telling you if the loader was called for the first time.

To disable automatic subsequent loader executions, you can just skip the request Signal. Our demo application disables both initial loading and subsequent for loading ratings:

ratingsResource = resource({
  // no request Signal === no automatic subsequent loading
  loader: (param) => {
    if (param.previous.status === ResourceStatus.Idle) {
      // skip initial loading!
      return Promise.resolve(undefined);
    }
    return this.#ratingService.loadExpertRatingsPromise();
  }
});

To trigger loading manually, we can call the resource's reload method. This allows us to provide a button for this task:

loadRatings(): void {
  this.ratingsResource.reload();
}

The reload method prevents overlapping requests by immediately returning if there is already a running one. Using terms from RxJS, this can be described as exhaustMap semantics.

Interlude 1: linkedSignal for Updates

The user can change the ratings. Normally, this is fine because a resource can be manually updated with its set and update methods used in a below section. However, a resource's value is undefined by default. At least since Angular applications use TypeScript's strict mode by default, I try to avoid null and undefined in favor of a default object, also known as Null Object. So far, I used computed to project null and undefined to the respective Null Object:

ratings = computed(() => this.ratingsResource.value() ?? {});

Unfortunately, a computed Signal is read only. The rescue is the linkedSignal function. It provides a computed Signal that can be changed:

ratings = linkedSignal(() => this.ratingsResource.value() ?? {});

If Signals used in the computation change, the computation is retriggered and the directly assigned value is overwritten. For the sake of completeness, I want to mention that the computation happens lazily: If no one is interested in the value, the recomputation does not happen.

This characteristic makes linkedSignal a perfect fit for forms: We can change a local copy and, on-demand, send it back for saving.

Error Handling

Also Error-Handling is baked into the resource API. When the loader throws or rejects the returned Promise, the resource switches into the state error. In this state, the error Signal provides the thrown value or the value passed to the Promise's reject function.

However, the resource will still proceed to work: If someone calls the reload method or if the request is changing, the loader is executed again. If the loader succeeds, the resource clears the error Signal and moves to the resolved state.

This is comparable to today's default behavior of Effects in NgRx, where some additional logic under the covers prevents the RxJS pipe used from stopping in the case of an error.

rxResource for RxJS-Interop

If you already have an Observable, you don't need to convert it into a Promise. Instead, you can use the rxResource API. It works like the resource but the loader is expected to return an Observable:

[...]

dessertsResource = rxResource({
  request: this.dessertsCriteria,
  loader: (param) => {
    return timer(300).pipe(switchMap(() => this.#dessertService.find(param.request)));
  }
});

[...]

ratingsResource = rxResource({
  loader: rxSkipInitial(() => {
    return this.#ratingService.loadExpertRatings()
  })
});

[...]

Also, the loader does not get an AbortSignal passed as Observables can be aborted (by unsubscribing implicitly or explicitly) in general.

It's important to know that the current implementation of rxResource is only using the first emitted value. Internally, it leverages firstValueFrom to convert the Observable to a Promise before handing over to resource. While this behavior might be ok for HTTP calls, it will result in a surprise when several values are received via a stream, e.g., when consuming Web Sockets.

We will see if this behavior will be changed over time. So far, I would use Observables emitting several values in a traditional way.

Resource in Services and Stores

The shown examples directly used the resource in a component. This was primarily for the sake of simplicity. Often, you will use resources in services or stores. One reason for this is state management: The loaded data will survive when Angular destroys the component at hand (e.g., because of switching to another route) so that it can be used later by an instance of the same or another component.

The second reason is that stores help to streamline your reactive dataflow:

This result in a so-called unidirectional data flow: The components send intentions to the store. I'm using the term 'intension' in an abstract way because it depends on the store, how it is expressed. When using a Redux store, it will be a triggered action; in a more lightweight store like the NGRX Signal Store, it might just be a called method.

The expressed intention makes the store perform some tasks that result in new values put into Signals. These Signals can be projected via computed and transport the data down to the component's view.

A very simple store is a service providing Signals. That means, we can move the code we've discussed so far into a service:

@Injectable({ providedIn: 'root' })
export class DessertStore {
  #dessertService = inject(DessertService);
  #ratingService = inject(RatingService);

  readonly originalName = signal('');
  readonly englishName = signal('');

  #dessertsCriteria = computed(() => ({
    originalName: this.originalName(),
    englishName: this.englishName(),
  }));

  #dessertsResource = resource({
    request: this.#dessertsCriteria,
    loader: debounce((param) => {
      return this.#dessertService.findPromise(param.request, param.abortSignal);
    })
  });

  #ratingsResource = resource({
    loader: skipInitial(() => {
      return this.#ratingService.loadExpertRatingsPromise();
    })
  });

  readonly loading = computed(() => this.#ratingsResource.isLoading() || this.#dessertsResource.isLoading());

  readonly desserts = computed(() => this.#dessertsResource.value() ?? []);
  readonly ratings = computed(() => this.#ratingsResource.value() ?? {});
  readonly ratedDesserts = computed(() => toRated(this.desserts(), this.ratings()));

  loadRatings(): void {
    this.#ratingsResource.reload();
  }

  updateRating(id: number, rating: number): void {
    this.#ratingsResource.update((ratings) => ({
      ...ratings,
      [id]: rating,
    }));
  }
}

Here, we see the component consuming the store:

@Component([...])
export class DessertsComponent {
  #store = inject(DessertStore);

  originalName = this.#store.originalName;
  englishName = this.#store.englishName;
  loading = this.#store.loading;

  ratedDesserts = this.#store.ratedDesserts;

  loadRatings(): void {
    this.#store.loadRatings();
  }

  updateRating(id: number, rating: number): void {
    this.#store.updateRating(id, rating);
  }
}

In this example, the unidirectional data flow can be seen quite clearly: The component sends up its intentions by calling methods and receives (updated) data via Signals bound in the template.

Updating Resources

Resources are not just read-only. We can also update them with a new value. While writing back changed values needs to be done manually, locally updating the resource ensures that the new value becomes part of our reactive flow. That means it will be displayed and projected, and it will be the foundation for further updates.

The following example shows a simple Service acting as a store for a details view. Please pay particular attention to the save method:

@Injectable({ providedIn: 'root' })
export class DessertDetailStore {
  #dessertService = inject(DessertService);
  #id = signal<number | undefined>(undefined);

  #dessertResource = resource({
    request: computed(() => ({ id: this.#id() })),
    loader: (param) => {
      const id = param.request.id;
      if (id) {
        return this.#dessertService.findPromiseById(id);
      }
      else {
        return Promise.resolve(initDessert);
      }
    },
  });

  readonly dessert = computed(() => this.#dessertResource.value() ?? initDessert);
  readonly loading = this.#dessertResource.isLoading;
  readonly error = this.#dessertResource.error;

  #saving = signal(false);

  load(id: number): void {
    this.#id.set(id);
  }

  save(dessert: Dessert): void {
    try {
      this.#saving.set(true);
      console.log('saving', dessert);
      // Here would be your HTTP Call
      [...]
      this.#dessertResource.set(dessert);
    }
    finally {
      this.#saving.set(false);
    }    
  }
}

The save method gets an updated dessert and writes it back to the server. The respective HTTP call is only indicated with a comment here. After performing this call, the dessertResource is updated with the new value using set. As an alternative, there is also an update method that allows to project the current value to a new one. If a loading process takes place while updating the resource with such a local value, this loading process is aborted.

In the example above, the resource was private to the service. Hence, the service can make sure the resource is updated only properly. If you want to expose the whole resource, you can use its asReadonly method to prevent consumers from changing the managed data:

readonly dessert = dessertResource.asReadonly();

Interlude 2: linkedSignal for the Details Form

When binding the loaded dessert to a template-driven form, we need writable Signals. However, stores usually provide read-only Signals. Also, the value Signal provided by resource is read-only, although it can be changed directly via the resource's set and update methods.

As already mentioned above, this is a good case for linkedSignal:

@Component({
  selector: 'app-dessert-detail',
  standalone: true,
  imports: [JsonPipe, RouterLink, FormsModule],
  templateUrl: './dessert-detail.component.html',
  styleUrl: './dessert-detail.component.css'
})
export class DessertDetailComponent implements OnChanges {
  store = inject(DessertDetailStore);

  id = input.required({
    transform: numberAttribute
  });

  loadedDessert = this.store.dessert;
  loading = this.store.loading;
  error = this.store.error;

  dessert = {
    originalName: linkedSignal(() => this.loadedDessert().originalName),
    englishName: linkedSignal(() => this.loadedDessert().englishName),
    kcal: linkedSignal(() => this.loadedDessert().kcal)
  };

  ngOnChanges(): void {
    const id = this.id();
    this.store.load(id);
  }

  save(): void {
    const dessert = {
      ...this.loadedDessert(),
      originalName: this.dessert.originalName(),
      englishName: this.dessert.englishName(),
      kcal: this.dessert.kcal(),    
    };

    this.store.save(dessert);
  }
}

Here, each property we want to bind to a form field is represented by a linked Signal. They are updated whenever their source is changing. Nevertheless, they can be updated with a local value. The save method takes this local value and writes it back to the store which delegates to the resource.

As a result, we can directly bind ngModel to our Signals:

<form>
  <div>
    <label for="englishName"> English Name </label>
    <input name="englishName" [(ngModel)]="dessert.englishName" />
  </div>
  <div>
    <label for="originalName"> OriginalName </label>
    <input name="originalName" [(ngModel)]="dessert.originalName" />
  </div>
  <div>
    <label for="kcal"> kcal </label>
    <input name="kcal" [(ngModel)]="dessert.kcal" />
  </div>
</form>

You can see these linked Signals as the counter part of the FormControl objects in reactive forms. Of course, FormControl objects provide more features like registering validators. But, and this is my personal impression, this shows that with Signals both approaches are not that far from each other.

While I decided to use template-driven forms in this example, you can totally leverage reactive forms, too. In this case, you might want to use an effect for connecting the Signals obtained from the store to your FormControls.

Bonus: Helper Functions for Streamlining

When implementing the demo application shown here, I also wrote some reactive helpers streamlining the work with Signals and resources. You find them in the provided demo repository in the branch 07c-final. For instance, the deepLink takes a Signal with an Object and converts all its properties to linked Signals:

dessert = deepLink(this.loadedDessert);

This prevents us from calling linkedSignal once per property, as in the example found in the previous section.

A debounce helper wrapping the loader takes care of debouncing:

dessertsResource = resource({
  request: this.dessertsCriteria,
  loader: debounce((param) => {
    return this.#dessertService.findPromise(param.request, param.abortSignal);
  })
});

It also respects the current AbortSignal. The default debounce time is 300 msec, but there is a parameter allowing us to specify another value.

Another helper is skipInitial which skips the initial call to the loader:

ratingsResource = resource({
  loader: skipInitial(() => {
    return this.#ratingService.loadExpertRatingsPromise();
  })
});

As a result, only subsequent changes to a potentially existing request or calls to reload trigger (re)loading.

Also, I wanted to prevent the loading indicator from showing up for quick loading operations. Hence, my debounceTrue helper uses a resource to debounce a transition from false to true:

loading = debounceTrue(() => this.ratingsResource.isLoading() || this.dessertsResource.isLoading(), 500);

It does not debounce a transition from true to false because we usually want the loading indicator to disappear immediately when the loaded resource becomes available.

Conclusion

The new Resource API is indeed a missing link in Angular's Signal story: It gives us an official way for loading asynchronous resources directly within the reactive flow. Also, it takes care of race conditions and contains basic error handling.

With this new API, we have an easy-to-use built-in feature for very common use cases. For more advanced scenarios, such as working with several parallel data streams, we can switch to something more powerful like RxJS. Thanks to the RxJS interop and rxResource, both worlds can be bridged.

eBook: Modern Angular

Bleibe am Puls der Zeit und lerne, moderne und leichtgewichtige Lösungen mit den neuesten Angular-Features zu entwickeln: Standalone, Signals, Build-in Dataflow.

Gratis downloaden