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!
Tomas Trajan
@tomastrajan
May 25, 2021
7 min read
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 😊I really like that title image. I'm surprised I haven't seen anything like that before.
— Ben Lesh 👈😎👈 (@BenLesh) April 3, 2021
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 getproductId
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:
re-create the form
re-subscribe to the
productType
value changes to perform side-effectThis 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!
❓❓❓❓❓
— 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... 😉🤗
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.
Obviously the bright future! (📸 by [Braden Jarvis](https://unsplash.com/@jarvisphoto?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText))And never forget, future is bright
Do you enjoy the theme of the code preview? Explore our brand new theme plugin
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 would like to learn more about how to ensure long term maintainability of your Angular application?
Angular Enterprise Architecture eBook
Learn how to architect a new or existing enterprise grade Angular application with a bulletproof tooling based automated architecture validation.
This will ensure that Your project stays maintainable, extendable and therefore with high delivery velocity over the whole project lifetime!
Get notified
about new blog posts
Sign up for Angular Experts Content Updates & News and you'll get notified whenever we release a new blog posts 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!
Responses & comments
Do not hesitate to ask questions and share your own experience and perspective with the topic
Tomas Trajan
Google Developer Expert (GDE)
for Angular & Web Technologies
I help developer teams deliver successful Angular applications through training and consulting with focus on Architecture and State managements with NgRx!
A Google Developer Expert for Angular & Web Technologies working as a consultant and Angular trainer. Currently empowering teams in enterprise organizations worldwide by implementing core functionality and architecture, introducing best practices, sharing know-how and optimizing workflows.
Tomas strives continually to provide maximum value for customers and the wider developer community alike. His work is underlined by a vast track record of publishing popular industry articles, leading talks at international conferences and meetups, and contributing to open-source projects.
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
Check out following blog posts from Angular Experts to learn even more about related topics like 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
@kreuzercode
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
@kreuzercode
Nov 18, 2023
3 min read
Empower your team with our extensive experience
Angular Experts have spent many years consulting with enterprises and startups alike, leading workshops and tutorials, and maintaining rich open source resources. We take great pride in our experience in modern front-end and would be thrilled to help your business boom