Following this single, straightforward RxJs guideline can help you and your team greatly streamline and thus enhance your projects!
Tomas Trajan
@tomastrajan
May 25, 2021
7 min read
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 😊I really like that title image. I'm surprised I haven't seen anything like that before.
— Ben Lesh 👈😎👈 (@BenLesh) April 3, 2021
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
switchMapto 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
QueryParamschanges to retrieveproductId - that
productIdtriggers a fresh stream of product types, which then populate the form, for instance, filling a dropdown… - the same
productIdalso 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
productTypefield 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:
reconstruct the form
re-subscribe to value changes of
productTypeto trigger the side-effectAt 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!
❓❓❓❓❓
— Tomas Trajan 🇨🇭 (@tomastrajan) March 30, 2021
What's the most common #RxJs BAD practice you see people using when working on #Angular projects?
❓❓❓❓❓
1⃣️not handling (un)subscription
2⃣️nested subscriptions
3⃣️re-creating streams during component lifetime
.
.
.
♾reply & share your experience... 😉🤗
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
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
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
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
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
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.
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!
Join the discussion
Feel free to ask anything and contribute your own insights and views on the subject
Tomas Trajan
Google Developer Expert (GDE)
for Angular & Web Technologies
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
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!

Tomas Trajan
@tomastrajan
Sep 10, 2024
15 min read

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

Kevin Kreuzer
@nivekcode
Jan 24, 2024
6 min read

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.

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
