RxJS in Angular – Antipattern 1 – Nested subscriptions

Working on numerous Angular projects over the past couple of years, it has become evident, that a lot of developers still have a hard time working with RxJS which finds its use in Angular. While RxJS is a very powerful tool, it comes with a lot of operators and a whole paradigm shift. Even experienced developers can be hung up on its intricacies and more than 100 operators.

In diesem Artikel:

yb
Yannick Baron ist Architekturberater bei Thinktecture mit einem Fokus auf Angular und RxJS.

In this first part of a small series, I want to share some pitfalls we discovered multiple times in code reviews over the years, as well a few patterns we identified as helpful.

Nested subscriptions in use

A very common use case is to request a data object from our API to then use some of its properties to request a few more related entities. Finally, we combine the received objects to fill our view.

In Angular we could implement it like this:

				
					this.route.params.pipe(
  switchMap(({ customerId }) => customerService.getCustomer(customerId)),
).subscribe(customer => {
  this.customer = customer;

  addressService.getAddress(customer.addressId)
    .subscribe(address => this.address = address);

  ordersService.getOrders(customer.orderIds)
    .subscribe(orders => this.orders = orders);
});
				
			

As implemented above, when the customerId in our route changes, we request a customer data object from the API. Once received, we use information on the object to request the related address data object and a couple of order records. All our data objects are assigned to a property on our component becoming part of the view.

We have encountered similar implementations on multiple occasions in code reviews and in the following I want to discuss downsides of this implementation as well as suggest a possible fix.

The Bad

Please notice how there are two subscribes nested in the outer subscribe on the getCustomer request.

In this case we have to handle and clean up three subscriptions (which we omitted in the above example for brevity). If we do not do this properly we can run into various problems, so please clean up after yourself!

With the code above we open up the possibility of a timing issue. Please assume the following sequence of events:

  1. our user navigates to the detail page of Customer1
  2. request Customer1 data object
  3. retrieve Customer1 and update view
  4. request address data for Customer1 … taking a while due to the API choking
  5. the impatient user of our application navigates to detail page of Customer2
  6. request Customer2 data object
  7. retrieve Customer2 and update view
  8. request address data for Customer2
  9. retrieve address data for Customer2 and update view
  10. retrieve address data for Customer1 and update view … finally resolved!

Due to the delay in the response of the first address, the view is updated with the first address after the second address has been loaded. This results in us displaying the second customer with the first customers’s address!

In the following, I tried to illustrate the above behavior in a more simplified fashion:

				
					// Emitting customerIds 1 and 2 with a 25ms delay
createStream<number>([1, 2], 25)
  .subscribe(id => {
    // Update the view with the received customer object
    updateView('customer', `Customer #${id}`)

    // Request address for customer but delay address for Customer #1 by 1000ms
    requestAddress(id)
      .subscribe(address => updateView('address', address));
  });
				
			

And you can run this implementation here:

You will find that the view is updated twice, as the first request for an address is delayed past the retrieval of the second customers’s information, thus resulting in the mismatch of information.

The Fix

In order to fix this behavior, we make use of the switchMap operator. After receiving the customer id (or the customer) we switch to the stream requesting the address. Furthermore, we use map to combine the customer and the retrieved address into a single object containing both. Therefore, our stream will carry this bundle of all the information we need to update our view in a single subscription, thus preventing a mismatch.

As an added bonus, switchMap unsubscribes from the inner stream for us, therefore, the request for the first address will be canceled, as soon as the second customer is emitted.

				
					// Emitting customerIds 1 and 2 with a 25ms delay
createStream<number>([1, 2], 25)
  // Switch stream to the address request
  .pipe(switchMap(id => {
    // Received customer object
    const customer = `Customer #${id}`; 

    return requestAddress(id)
      // Combine the customer object and the resolved address to a single object
      .pipe(map(address => ({ customer, address })));
  }))
  .subscribe(({ customer, address }) => {
    // Update the view with the matching information
    updateView('customer', customer);
    updateView('address', address);
  });

				
			

Please see this fix for yourself:

Bonus

As a little bonus I recreated the initial problem statement more faithfully. In this demo I am simulating a request for a customer object, and requesting multiple related entities, making use of zip:

Conclusion

We have illustrated how nesting subscriptions can result in timing issues causing unforeseen side-effects. Furthermore, the more subscriptions you have, the more you have to manage unsubscribing, cleanup actions and handling errors. Most of the time the goal should be to use the operators RxJS provides us with to combine our streams to yield a single value. This makes it easier to argue about how our data flows through the stream with no added side-effects.

In the next part of this series we want to discuss another rather common pitfall we have encountered in various reviews and how employing above techniques will help us a different form of potential side-effects.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Low-angle photography of metal structure
AI
cl-neu

AI-Funktionen zu Angular-Apps hinzufügen: lokal und offlinefähig

Künstliche Intelligenz (KI) ist spätestens seit der Veröffentlichung von ChatGPT in aller Munde. Wit WebLLM können Sie einen KI-Chatbot in Ihre eigenen Angular-Anwendungen integrieren. Wie das funktioniert und welche Vor- und Nachteile WebLLM hat, lesen Sie hier.
26.02.2024
Angular
SL-rund

Konfiguration von Lazy Loaded Angular Modulen

Die Konfigurierbarkeit unserer Angular-Module ist für den Aufbau einer wiederverwendbaren Architektur unerlässlich. Aber in der jüngsten Vergangenheit hat uns Angular seine neue modullose Zukunft präsentiert. Wie sieht das Ganze jetzt aus? Wie konfigurieren wir jetzt unsere Lazy-Komponenten? Lasst uns gemeinsam einen Blick darauf werfen.
03.08.2023
Angular
yb

Using EntityAdapter with ComponentStore: @ngrx/entity Series – Part 3

As someone who enjoys the ComponentStore on an average level, I have written simple reactive CRUD logic several times. While storing a vast number of entities in the component state might not be a frequent use case, I will briefly illustrate the usage of the EntityAdapter with the @ngrx/component-store.
14.02.2023
Angular
yb

Multiple Entity Collections in the Same Feature State: @ngrx/entity-Series – Part 2

After introducing the @ngrx/entity package, I am often asked how to manage multiple entity types in the same feature state. While I hope that the previous part of this article series has made this more apparent, I will further focus on this question in the following.
07.02.2023
Angular
yb

Managing Your Collections With the EntityAdapter: @ngrx/entity-Series – Part 1

This three-part series of blogposts is targeted at developers who have already gained experience with NgRx but still manage their collections themselves. In the first part I introduce the Entity Adapter, in the second part I show you how to connect it to NgRx and in the third part how to do it with the Component Store as well.
31.01.2023
Angular
MS-rund

Implementing Smart and Presentational Components with Angular: Condensed Angular Experiences – Part 4

In this article, we will explore how to apply the concept of smart and presentational components with Angular. We will choose a complex-enough target to see all aspects in action, yet understandable and within the scope of this article. The goal is to teach you how to use this architecture in your way. For that, we will iterate through different development stages, starting with the target selection and implementing it in a naive way. After the first development, we will refactor that naive solution into smart and presentational components that are reusable, refactor-friendly, and testable.
23.01.2023