The Most Impactful RxJs Best Practice Of All Time

This one easy to follow RxJs tip can help YOU and your teammates to simplify and hence improve your projects significantly!

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

25.05.2021

7 min read

The Most Impactful RxJs Best Practice Of All Time
share

There be RxJs stream monsters, but no worries, we got this!
(Original 📷 by Laura Smetsers | Design 🎨 by Tomas Trajan)

The one and only Ben Lesh made my day be digging the intro pic 😊

OK, OK, the heading might be a bit sensational but hear me out… This easy to follow tip can help YOU and your teammates to simplify and hence improve your projects significantly!

💎 The article is based on extended experience from a large enterprise environment with more than 140 Angular SPAs and 30 libraries…

🤫 Wanna know how we can manage such a large environment without going full crazy 😵 Check out Omniboard!😉

As you can probably imagine, such an environment also translates into lots of people working tirelessly on the codebases of those projects to deliver new features for their users! These people often have to work in full-stack mode to be able to meet everchanging demands and prioritization.

This lack of focus also often goes hand in hand with little experience and hence unfamiliarity with reactive programming which is pretty different from more common imperative way of doing things!

👨🍳️ To summarize, following ingredients:

  • huge amount of projects
  • full-stack development mode and lack of focus
  • unfamiliarity with reactive programming

provide us with opportunity to uncover some recurring patterns of RxJs usage that may look OK on the surface but will lead to problems and hard to understand code if left unchecked!

The Problem

One of the most common and straight forward problem to solve is situation when RxJs streams are being re-created during component (or service) lifetime as a reaction to some user interaction or event like receiving response from a backend…

TLDR; THE TIP

It is ALWAYS possible to FULLY define a RxJs stream from the start! We can include all the possible sources of change in the initial stream definition. Because of this, it is NEVER necessary to re-create a stream during the component (or service) lifetime!

There, we said it… and it’s true! Now we are going to explore what this means by providing examples and guidelines how to apply this tip when developing your Angular applications!

Example: Product Chooser

Imagine we are building an application which allows users to choose one of the available products. After user selection, the application should load and display selected product information…

One of the ways to implement such an use case is illustrated by the simplified code snippet bellow…

@Component({
  template: ` <!-- some nice product cards -->
    <button (click)="selectProduct(1)"> Product 1 </button>
    <button (click)="selectProduct(2)"> Product 2 </button>
    <button (click)="selectProduct(3)"> Product 3 </button>

    <product-info *ngIf="product$ | async as product" [product]="product">
    </product-info>`,
})
export class ProductChooser {
  product$: Observable<Product>;

  constructor(private productService: ProductService) {}

  selectProduct(productId: number) {
    this.product$ = this.productService.loadProduct(productId);
  }
}

Example of re-creation of RxJs stream as a result of user interaction

The example above re-assigns (and re-creates) products stream every time user clicks on a button to select desired product. The stream is then re-subscribed in the template using the | async pipe.

Let’s compare the above approach with the following…

@Component({
  template: ` <!-- some nice product cards -->
    <button (click)="selectProduct(1)"> Product 1 </button>
    <button (click)="selectProduct(2)"> Product 2 </button>
    <button (click)="selectProduct(3)"> Product 3 </button>

    <product-info *ngIf="product$ | async as product" [product]="product">
    </product-info>`,
})
export class ProductChooser {
  selectedProductUd$ = new Subject<number>();

  product$ = this.selectedProductUd$.pipe(
    switchMap((productId) => this.productService.loadProduct(productId)),
  );

  constructor(private productService: ProductService) {}

  selectProduct(productId: number) {
    this.selectedProductId$.next(productId);
  }
}

Example of RxJs stream definition which includes all the sources of change from the beginning…

Our this.product$ stream is defined ONLY ONCE in the property assignment and will NOT change for the lifetime of the component.

The stream is mapping selected product ID into a response of the backend call to load product info (with help of flattening operator switchMap which subscribes to the inner observable, our backend request)

Now, exact way of writing this does not really matter. We could have also used our selectedProductId$ Subject directly in the template to make the code even more succinct…

@Component({
  template: ` <!-- some nice product cards -->
    <button (click)="selectedProductId$.next(1)"> Product 1 </button>
    <button (click)="selectedProductId$.next(2)"> Product 2 </button>
    <button (click)="selectedProductId$.next(3)"> Product 3 </button>

    <product-info *ngIf="product$ | async as product" [product]="product">
    </product-info>`,
})
export class ProductChooser {
  selectedProductUd$ = new Subject<number>();

  product$ = this.selectedProductUd$.pipe(
    switchMap((productId) => this.productService.loadProduct(productId)),
  );

  constructor(private productService: ProductService) {}
}

Example of RxJs stream definition which includes all the sources of change from the beginning with more succinct implementation when we use Subject directly in the template…

Now, you may very well say that it does NOT make much of a difference in such a simplified scenario and you would be right…

Simple re-assigning of the this.product$ stream in a plain on-liner method which reacts to an user click is very easy to scan visually and understand.

Unfortunately, such an approach can get out of hand very fast…

Follow me on Twitter because you will get notified about new Angular blog posts and other cool frontend stuff!😉

Realistic Example

How does the stream re-creation approach look in a more realistic scenario? Let’s have a look on the following example inspired by code found in a real world project…

@Component(/* ... */)
export class ComplexProductChooser implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  productForm: FormGroup;
  productTypes$: Observable<ProductType[]>;

  constructor(private activatedRoute: ActivatedRoute, /* ... */) {}

  ngOnInit() {
    this.activatedRoute.params
      .pipe(takeUntil(this.destroy$))
      .subscribe(params => {
        const { productId } = params;
        this.productTypes$ = this.productService.getTypes(productId);
        this.productForm = this.buildForm(productId);
        this.productForm.get('productType').valueChanges
          .pipe(takeUntil(this.destroy$))
          .subscribe(productType =>
            this.sidebarService.loadAndDisplayContextualProductInfo(productType);
          );
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Example of re-creation of RxJs streams during component lifetime as a reaction to changes in the URL and user interaction (selecting product type in a form)…

This example is a bit dense so let’s unpack what is going on:

  • component listens to the changes in QueryParams to get productId
  • productId is then used to re-create stream of product types which are used in the form, for example to populate a dropdown…
  • productId is also used to re-create form definition, for example because there might be some logic to conditionally display some fields based on ID ranges…
  • the newly created form and especially changes of its productType field are then used to perform a side-effect, updating sidebar with some contextual information for the selected product type

Let’s now explore the same example with comments…

@Component(/* ... */)
export class ComplexProductChooser implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  productForm: FormGroup;
  productTypes$: Observable<ProductType[]>;

  constructor(private activatedRoute: ActivatedRoute, /* ... */) {}

  ngOnInit() {
    // react to changes in query params
    this.activatedRoute.params
      .pipe(takeUntil(this.destroy$))
      .subscribe(params => {
        // retrieve product ID from query params
        const { productId } = params;

        // re-create and re-asign stream of product types to be used in form dropdown
        this.productTypes$ = this.productService.getTypes(productId);

        // re-create form
        this.productForm = this.buildForm(productId);

        // listen to changes of product Type form field to perfom side-effect
        this.productForm.get('productType').valueChanges
          // when does this happen?
          // how many active streams do we end up with potentailly?
          .pipe(takeUntil(this.destroy$))
          .subscribe(productType =>
            // perform side-effect
            this.sidebarService.loadAndDisplayContextualProductInfo(productType);
          );
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Example of re-creation of RxJs streams during component lifetime as a reaction to changes in the URL and user interaction (selecting product type in a form) with comments to illustrate previously made points…

Previously we just described what the implementation does, but what is wrong with it?

The most jarring problem is with performing that side-effect as a response to changes of the selected productType. As we can see, every time the productId changes, we perform the following:

  1. re-create the form

  2. re-subscribe to the productType value changes to perform side-effect

  3. This subscription looks good on the surface, it even uses takeUntil(this.destroy) so it must be ok, right? After all, that’s the preferred declarative way of cleaning up subscriptions…

Well, as you have guessed, wrong! 💀

As we keep re-creating the form and re-subscribing to the productType changes stream based on the new productId value, we will keep adding more and more active subscriptions trying to perform desired side-effect at the same time!

This will lead at best to performance problems and at worst to buggy behavior when we will display out of sync data in the side bar based on which invocation of the side effect finishes last, for example depending on the network conditions!

The Right Way To Implement Your RxJs Streams

As we asserted earlier, it is ALWAYS possible to define whole RxJs stream including all sources of change from the start so that’s exactly what we’re going to do now!

Let’s see how we can rewrite previous example using this principle and what will be the benefits of doing so…

@Component(/* ... */)
export class ComplexProductChooser implements OnInit, OnDestroy {
  productForm$: Observable<FormGroup>; // will be subscribed in tpl with | async pipe
  productTypes$: Observable<ProductType[]>; // will be subscribed in tpl with | async pipe

  constructor(private activatedRoute: ActivatedRoute /* ... */) {}

  ngOnInit() {
    // define stream of productId
    const productId$ = this.activatedRoute.params.pipe(
      map((params) => params.productId),
    );

    // define stream of product types (switchMap because getTypes returns Observable)
    this.productTypes$ = productId$.pipe(
      switchMap((productId) => this.productService.getTypes(productId)),
    );

    // define stream of forms (map because this.buildForm is a sync method)
    this.productForm$ = productId$.pipe(
      map((productId) => this.buildForm(productId)),
    );

    // define stream to perform side-effect, only ONE stream instance will exist
    // listen to changes of productType form field to perfom side-effect
    this.productForm$
      .pipe(
        // switchMap because we want to perform side-effect only for the latest form
        switchMap((form) => form.get('productType').valueChanges),
        takeUntil(this.destroy$),
      )
      .subscribe((productType) =>
        // perform side-effect
        this.sidebarService.loadAndDisplayContextualProductInfo(productType),
      );
  }

  // ...
}

Example of proper definition of RxJs stream including all sources of change from start

We’re defining all the RxJs streams including all sources of change from start!

Everything that will ever happen in this component is expressed declaratively in a single location

More so, we can completely forget about the notion of time when trying to understand what this component is doing as we can see whole picture form the get go!

Besides that, we’re also solving our previous issue when our side-effect related stream existed in multiple instances which caused performance issues and even inconsistent state and hence bugs from the user point of view…

Attention: Common Gotcha! ⚠️

🙏 Please, please, please DO NOT start to introduce RxJs streams in your sync logic just because we refactored selectProduct method from plain method to a Subject in the initial example.

The refactor in the initial example was caused because we needed to handle already existing RxJs stream (for backend request) so in that case it makes sense to express the change to which the stream has to react as a part of that stream.

Do not introduce RxJs streams into the logic that is (or can be) fully synchronous as it is not necessary and will make your code more complicated and harder to maintain!

Bonus: Voice of the People

Check out, what do people think is the most common RxJs bad practice and their experiences in the replies to this tweet!

Great, we have made it to the end! 🔥

I hope you enjoyed learning about the one of the most impactful RxJs best practice tip that you can use in your projects to deliver great applications for your users!

Also, don’t hesitate to ping me if you have any questions using the article responses or Twitter DMs @tomastrajan.

And never forget, future is bright

Obviously the bright future! 📸 by Braden Jarvis Obviously the bright future! (📸 by [Braden Jarvis](https://unsplash.com/@jarvisphoto?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText))

Do you enjoy the theme of the code preview? Explore our brand new theme plugin

Skol - the ultimate IDE theme

Skol - the ultimate IDE theme

Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.

Do you enjoy the content and think that your teammates or organization could benefit from more direct support?

Angular Mastery Workshop

Angular Mastery Workshop

Want to become an Angular expert and deliver maintainable features confidently and on time?

This workshop will teach you all necessary concepts to become proficient Angular developer by building a real world single page application!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever I release a new article about Angular, Ngrx, RxJs or other interesting Frontend topics!

We will never share your email with anyone else and you can unsubscribe at any time!

Emails may include additional promotional content, for more details see our Privacy policy.
Tomas Trajan - GDE for Angular & Web Technologies

Tomas Trajan

GDE for Angular & Web Technologies

I help developer teams deliver successful Angular applications through training and consulting with focus on NgRx, RxJs and Nx!

Ein Google-Developer expert für Angular und Webtechnologien, der als Berater und Angular-Trainer arbeitet. Derzeit unterstützt er weltweit Teams in Unternehmen bei der Implementierung von Kernfunktionalitäten, Architekturen, der Einführung von Best Practices, und der Optimierung von Arbeitsabläufen.

Tomas ist ständig bestrebt, seinen Kunden und der breiteren Entwicklergemeinschaft einen maximalen Nutzen zu bieten. Seine Arbeit wird durch eine umfangreiche Erfolgsbilanz unterstrichen, in der er populäre Fachartikel veröffentlicht, Vorträge auf internationalen Konferenzen und Meetups hält und zu Open-Source-Projekten beiträgt.

45

Blog posts

4M

Blog views

3.5K

Github stars

500

Trained developers

30

Given talks

8

Capacity to eat another cake

Responses

Do not hesitate to ask questions and share your own experience and perspective with the topic

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like RxJs or Angular !

Stärken Sie Ihr Team mit unserer umfassenden Erfahrung

Unsere Angular Experten haben viele Jahre damit verbracht, Unternehmen und Startups zu beraten, Workshops und Tutorials zu leiten und umfangreiche Open-Source-Ressourcen zu pflegen. Wir sind sehr stolz auf unsere Erfahrung im Bereich des modernen Frontends und würden uns freuen auch Ihrem Unternehmen zum Aufschwung zu verhelfen.