Following this single, straightforward RxJs guideline can help you and your team greatly streamline and thus enhance your projects!

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

May 25, 2021

7 min read

The Most Impactful RxJs Best Practice Of All Time
share

RxJs streams might feel like monsters, but don't worry, we've got this covered!
(Original 📷 by Laura Smetsers | Design 🎨 by Tomas Trajan)

The legendary Ben Lesh made my day by appreciating the intro image 😊

Alright, the title might be a tad dramatic, but stick with me… This straightforward advice can help YOU and your colleagues simplify and thereby significantly enhance your projects!

💎 This article draws from hands-on experience in a massive enterprise setting featuring more than 140 Angular SPAs and 30 libraries…

🤫 Curious how we manage such a vast ecosystem without losing our minds 😵 Check out Omniboard!😉

As you might expect, such a scale naturally involves many people working intensively across those codebases to ship new features to users! These developers frequently need to operate in full-stack mode to keep up with shifting requirements and priorities.

This scattered focus often comes with limited experience and, consequently, minimal exposure to reactive programming, which differs quite a bit from the more conventional imperative style!

👨🍳️ In short, here’s what we’re working with:

  • vast number of projects
  • full-stack development method and scattered focus
  • limited familiarity with reactive programming

These elements give us the chance to identify recurring patterns in RxJs usage that might appear fine at first glance but can cause issues and confusing code if not addressed!

The Problem

One of the most frequent and straightforward challenges involves RxJs streams being re-created throughout a component (or service) lifetime, triggered by user actions or events such as a backend response…

TLDR; THE TIP

Whenever possible, we should FULLY define a RxJs stream from the outset! Every potential source of change can be incorporated into the initial stream setup. As a result, re-creating a stream mid-lifetime of a component (or service) is NEVER required!

There, we've said it… and it holds true! Now, let's dive into what this means with examples and guidelines for applying this tip while developing Angular applications!

Example: Product Chooser

Picture this: we're building an app that lets users pick any product from a catalog. Once a product is selected, the app should fetch and show the details for that product…

One approach to handle this scenario is shown in the simplified code snippet below…

@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);
  }
}

Illustrative re-instantiation of an RxJs stream triggered by a user action

In the prior snippet, the products stream is re-initialized and overwritten on each click event, whenever the user chooses a product. Subsequently, the template subscribes anew to it via the | async pipe.

Now, consider the alternative pattern below…

@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);
  }
}

Here's an example of an RxJs stream that has all change sources baked in right from the start…

Our this.product$ gets set only once during property assignment, and it remains unchanged across the component's entire lifecycle.

That stream takes the selected product ID and turns it into a backend response that loads product information, using the flattening operator switchMap to handle the inner observable, which is the HTTP request.

The precise implementation here is not the point. We might have just used our selectedProductId$ Subject straight in the template, making the code shorter…

@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) {}
}

Here’s a sample of an RxJs stream setup where all change sources are defined upfront, and the implementation gets more concise when we assign the Subject directly in the template…

You might argue that in such a basic case, this hardly matters—and you’d be correct...

Reassigning the this.product$ stream within a straightforward one-liner that responds to a user’s click is straightforward to scan and comprehend.

However, this kind of pattern can spiral out of control quite quickly...

Follow me on Twitter so you stay updated on fresh Angular posts and other cool frontend content!😉

Realistic Example

Let’s see how stream re-creation plays out in a more complex setting. Check out this example, which draws from actual code seen in a production 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();
  }
}

A demonstration of RxJs streams being rebuilt over a component's lifecycle in reaction to URL updates and user actions, such as selecting a product type in a form…

This illustration packs a lot of detail, so let's break it down step by step:

  • the component subscribes to QueryParams changes to retrieve productId
  • that productId triggers a fresh stream of product types, which then populate the form, for instance, filling a dropdown…
  • the same productId also regenerates the form's definition—maybe certain fields need to appear or disappear based on ID ranges…
  • once that form exists, any modifications to its productType field drive a side-effect that refreshes the sidebar with context-specific info for the chosen product type

Now, let's walk through the same scenario but with annotations added…

@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();
  }
}

Illustration of RxJs stream recreation during a component’s lifecycle triggered by URL updates and user input (selecting a product type in a form), with remarks tying back to the points made earlier.

So far, we’ve explained what the code does—but what’s actually wrong with it?

The most noticeable issue is executing that side-effect whenever the selected productType changes. Notice that each time the productId changes, we do the following:

  1. reconstruct the form

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

  3. At first glance, this subscription appears fine—it even employs takeUntil(this.destroy), so it must be safe, right? After all, that’s the recommended declarative approach for managing subscription cleanup…

But as you’ve probably figured out, that’s incorrect! 💀

Because we keep rebuilding the form and re-subscribing to the productType change stream for each new productId, we end up with an ever-growing pile of active subscriptions all trying to execute the same side-effect simultaneously!

This results in, at best, degraded performance, and, at worst, unpredictable behavior—such as showing outdated data in the sidebar depending on which side-effect call finishes last, say, under varying network conditions!

The Proper Way To Set Up Your RxJs Streams

As we mentioned earlier, it is always feasible to define the entire RxJs stream, including all change sources, from the outset—and that’s precisely what we’ll do now!

Let’s explore how we can refactor the previous example using this perspective, and what advantages that will bring…

@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),
      );
  }

  // ...
}

An example of a properly defined RxJs stream, covering every source of change from the very beginning

Here, we're setting up all the RxJs streams upfront, including every source of change from the start!

All future events in this component are declaratively described in one place

Moreover, you can discard the concept of time when deciphering this component—the entire picture is visible at a glance!

Additionally, this strategy resolves the earlier problem where our side-effect-related stream was duplicated in several spots, leading to performance hiccups, inconsistent states, and hence bugs from the user's perspective…

Caution: Common Pitfall! ⚠️

🙏 Please, please, please DO NOT add RxJs streams to your synchronous logic merely because we changed the selectProduct method from a plain function to a Subject in the first example.

That refactor was necessary since we had to manage an existing RxJs stream (for the backend request), so it’s logical to integrate the change triggering that stream directly into it.

Avoid introducing RxJs streams into fully synchronous (or potentially synchronous) logic—it’s unnecessary and only adds complexity, making your code harder to maintain!

Extra: Community Insights

Explore what folks identify as the top RxJs anti-patterns and their real-world encounters in the responses to this tweet!

We've reached the conclusion! 🔥

I trust you've gained valuable insights into this impactful RxJs best practice that you can apply to build exceptional user-facing applications!

Feel free to reach out anytime via article comments or Twitter DMs @tomastrajan.

And always remember, a bright future awaits

Obviously the bright future! 📸 by Braden Jarvis Needless to say, what lies ahead is rather brilliant! (📸 by [Braden Jarvis](https://unsplash.com/@jarvisphoto?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText))

Are you into the look of the code snippet? Check out our newly crafted theme plugin

Skol - the top-tier IDE theme

Skol - the ultimate IDE theme

Experience the aurora borealis right inside your development environment. This minimal yet potent dark theme is both aesthetically pleasing and gentle on your eyes.

Craft more intelligent interfaces by combining Angular with artificial intelligence

Learn Angular and AI Through Video Lessons

Angular + AI Video Course

A practical workshop demonstrating AI integration into Angular applications via Hash Brown, enabling intelligent and responsive user interfaces.

Explore real-time chat streams, tool invocation, dynamic UI generation, structured outputs, and further topics — all in a guided sequence.

If you find this valuable and see potential advantages for your team or company through targeted assistance, what next?

Reactive RxJs Workshop

Getting reactive with RxJs Workshop

Among all the moving parts in Angular development, RxJs remains the hardest one to wrap your head around.

Boost your team's RxJs proficiency so you can ship robust, maintainable features without the usual guesswork!

Are you a fan of the content and ready to dig deep into Angular's latest Signal Forms?

Angular Signal Forms: Hands-On Masterclass

Angular Signal Forms: Hands-On Masterclass

Dive into Angular's freshly introduced Signal-Forms across twelve incremental chapters, blending conceptual explanations with practical exercises.

Explore everything from the fundamentals of forms and validators to building custom controls, managing subforms, and planning your upgrade path.

Win win deal illustration

Stay in the loop
with fresh articles

Subscribe to Angular Experts Content Updates & News and receive a notification every time we publish a new post on Angular, Ngrx, RxJs, or other exciting Frontend subjects!

Your email stays private and cancelling your subscription takes just a click!

Emails might carry extra promotional material; check our Privacy policy for specifics.

Join the discussion

Feel free to ask anything and contribute your own insights and views on the subject

Tomas Trajan - GDE for Angular & Web Technologies

Tomas Trajan

Google Developer Expert (GDE)
for Angular & Web Technologies

Google Developer Experts logo X logo LinkedIn logo Github logo Github logo Spotify logo Medium logo public

My consulting and training services help development teams ship successful Angular applications, with a particular emphasis on Architecture and State management using NgRx!

As a Google Developer Expert for Angular & Web Technologies, I work both as a consultant and an Angular trainer. My current focus is on enabling enterprise teams globally by building core architecture, instilling best practices, transferring knowledge, and refining development processes.

Tomas is dedicated to delivering high value both to clients and the broader coding community. This is reflected in a long history of producing popular industry articles, presenting at international conferences and meetups, and making contributions to open-source initiatives.

52

Blog posts

4.7M

Blog views

3.5K

Github stars

612

Trained developers

39

Given talks

8

Capacity to eat another cake

You might also like

Take a look at these other Angular Experts articles to deepen your understanding of related subjects such as RxJs or Angular !

Top 10 Angular Architecture Mistakes You Really Want To Avoid

Top 10 Angular Architecture Mistakes You Really Want To Avoid

In 2024, Angular keeps changing for better with ever increasing pace, but the big picture remains the same which makes architecture know-how timeless and well worth your time!

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

Sep 10, 2024

15 min read

Angular Signal Inputs

Angular Signal Inputs

Revolutionize Your Angular Components with the brand new Reactive Signal Inputs.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Jan 24, 2024

6 min read

Improving DX with new Angular @Input Value Transform

Improving DX with new Angular @Input Value Transform

Embrace the Future: Moving Beyond Getters and Setters! Learn how to leverage the power of custom transformers or the build in booleanAttribute and numberAttribute transformers.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Nov 18, 2023

3 min read

Put our collective know-how to work for your team

Through years of collaboration with enterprises, startups, and educational initiatives, we have built a depth of practical insight into modern front-end development. Our workshops, tutorials, and open source contributions reflect that expertise—and we are eager to put it to use for your growth