With RxJS being a prominent member of the Angular framework, you're going to run into it one way or another. If you venture into the world of NGRX for state management, you can't write applications without working with Observables. This is supposed to lead to blazing fast reactive applications, with a predictable direction of flow within your application.
Data flows down, events bubble up.
This, however, is not always the case. As you throw yourself head first into the world of RxJS, operators and hard to understand docs, you can find yourself in a world of performance trouble and memory leaks. In the following examples I'll outline some patterns useful and harmful when working with data in your components. The premise of the examples is simple - a list of data fetched from store, and the ability to highlight an item, and display the count.
Disclaimer: the following 3xamples are handwritten in markdown, and may contain syntax errors and may not run directly. They are for illustration purposes only
Using .subscribe(...)
One of the first patterns I came across when I started this, was the .subscribe()
method. It seems harmless to just subscribe to the observable and assign the value to a private or public property:
@Component(
selector: 'my-component',
template: `
<div>Number of items: {{ numberOfItems }}</div>
<ul>
<li [class.selected]="isSelected(item)"
(click)="select(item)"
*ngFor="let item of manyItems">
{{ item.name }}
</li>
</ul>
`,
changeDetectionStrategy: ChangeDetectionStrategy.OnPush
)
export class MyComponent {
manyItems: { [key: string]: SomeObject };
numberOfItems: number;
selectedItem: SomeObject;
constructor(private store: Store<any>) { }
ngOnInit() {
this.store.select(selectManyItems).subscribe(items => {
this.manyItems = items;
this.numberOfItems = items.lenght;
});
this.store.select(selectedItem).subscribe(
item => this.selectedItem = item
)
}
public select(item) {
this.store.dispatch(selectItem(item));
}
}
This approach may seem fine, but it's a disaster waiting to happen. Since subscriptions like this are not automatically unsubscribed, they will continue to live on, even if MyComponent
is disposed and destroyed.
If you really have to use .subscribe()
, you have to unsubscribe manually!
Using .subscribe(...) and takeUntil(...)
One way of achieving this would be to keep a list of all subscriptions, and manually unsubscribing those in ngOnDestroy()
, but that's also prone for error. It's easy to forget a subscription, and then you're in the same situation as above.
We can achieve a proper unsubscription by introducing the takeUntil(...)
operator for our subscriptions.
@Component(
selector: 'my-component',
template: `
<div>Number of items: {{ numberOfItems }}</div>
<ul>
<li [class.selected]="isSelected(item)"
(click)="select(item)"
*ngFor="let item of manyItems">
{{ item.name }}
</li>
</ul>
`,
changeDetectionStrategy: ChangeDetectionStrategy.OnPush
)
export class MyComponent {
manyItems: { [key: string]: SomeObject };
numberOfItems: number;
selectedItem: SomeObject;
destroyed$ = new Subject();
constructor(private store: Store<any>) { }
ngOnInit() {
this.store.select(selectManyItems)
.pipe(takeUntil(this.destroyed$))
.subscribe(items => {
this.manyItems = items;
this.numberOfItems = items.lenght;
});
this.store.select(selectedItem)
.pipe(takeUntil(this.destroyed$))
.subscribe(
item => this.selectedItem = item
);
}
ngOnDestroy() {
this.destroyed$.next();
}
public select(item) {
this.store.dispatch(selectItem(item));
}
}
In this example, we're still setting our private and public properties, but by emitting on the destroyed$
subject in ngOnDestroy()
we make sure the subscriptions are unsubscribed when our component is disposed.
I'm not a big fan of the subscribe()
method within my Angular components, as it feels like a smell. I just can't rid the feeling that I'm doing something wrong, and that subscribe()
should be a last resort of some kind.
Luckily Angular gives us some automagical features that can help us handle the observables in a more predictable manner without unsubscribing ourselves.
Using the async pipe
The async
pipe takes care of alot of the heavy lifting for us, as it takes an Observable
as the input, and triggers change whenever the Observable emits. But the real upside with async
is that it will automatically unsubscribe when the component is destroyed.
@Component(
selector: 'my-component',
template: `
<div>Number of items: {{ numberOfItems$ | async }}</div>
<ul>
<li [class.selected]="(selectedItem$ | async) === item"
(click)="select(item)"
*ngFor="let item of manyItems$ | async">
{{ item.name }}
</li>
</ul>
`,
changeDetectionStrategy: ChangeDetectionStrategy.OnPush
)
export class MyComponent {
manyItems$: Observable<{ [key: string]: SomeObject }>;
numberOfItems$: Observable<number>;
selectedItem$: Observable<SomeObject>;
constructor(private store: Store<any>) { }
ngOnInit() {
this.manyItems$ = this.store.select(selectManyItems);
this.selectedItem$ = this.store.select(selectedItem);
this.numberOfItems$ = this.manyItems$.pipe(
map(items => items.length)
);
}
public select(item) {
this.store.dispatch(selectItem(item));
}
}
Now this seems better. But what we gained in protection against memory leaks, we've lost in the readability in the template. The template is soon riddled with async
pipes all over the place, and you'll end up writing lots of *ngIf="myItems$ | async as myItems"
to cater for complexity. Though this is just fine in small templates, it can grow and become hard to handle.
Another caveat with this approach is that you might require combining, zipping, merging your Observables, leading to RxJS spaghetti which is extremely hard to maintain, let alone read.
(If you're using NGRX like in the example code, this can also be avoided by properly mastering selectors!)
What I've moved towards in my ventures is container components.
Container components
By using container/presentation components (dumb/smart, or whatever you'd like to call them), we can separate the conserns even more. Leveraging the async
pipe once again, we can keep our Observable alone in our container component, letting the child component do what needs to be done.
@Component(
selector: 'my-container',
template: `<child-component (selectItem)="select(item)" [items]="manyItems$ | async"></child-component>`
)
export class MyContainerComponent implements OnInit {
manyItems$: Observable<{ [key: string]: SomeObject }>
selectedItem$: Observable<SomeObject>;
constructor(private store: Store<any>) { }
ngOnInit() {
this.manyItems$ = this.store.select(selectManyItems);
this.selectedItem$ = this.store.select(selectedItem);
}
select(item) {
this.store.dispatch(selectItem(item));
}
}
Our container component now only contains the selectors from our store, and we don't have to care about anything but to pass that on to our child component with the async
pipe. Which makes our child component extremely light weight.
@Component(
selector: 'child-component',
template: `
<div>Number of items: {{ numberOfItems }}</div>
<ul>
<li [class.selected]="isSelected(item)"
(click)="selectItem.emit(item)"
*ngFor="let item of manyItems">
{{ item.name }}
</li>
</ul>
`,
changeDetectionStrategy: ChangeDetectionStrategy.OnPush
)
export class ChildComponent {
@Input() manyItems: SomeObject[];
@Input() selectedItem: SomeObject;
@Output() selectItem = new EventEmitter<SomeObject>();
public get numberOfItems() {
return this.manyItems?.length ?? 0;
}
public isSelected(item) {
this.selectedItem === item;
}
}
Imporant note: Remember to always use ChangeDetection.OnPush! This causes Angular to run changedetection only when the reference values of your Inputs change, or when an Output emits. Otherwise evaluating methods and getters in your template will be a major performancehit!
Our child component now have all the same functionality as all the other examples, but the template has better readability, and the component has no dependencies. Testing this component with plain Jasmine specs is now lightning fast, and simple to do, without TestBeds, mocks or other boilerplate testsetup.
The added benefit here, is that you now have a ChildComponent that is completely obvlivious as to how it gets the data it's supposed to display, making it reusable and versatile.
Another bonus is that you don't have to introduce new observables with maps and filters, in order to do further work with your data:
@Component(
selector: 'blog-post-list-component',
template: `
<div>Number of blogposts: {{ numberOfBlogposts }}</div>
<div>Number of published blogposts : {{ numberOfPublishedBlogPosts }}</div>
<ul>
<li [class.selected]="isSelected(post)"
(click)="selectPost.emit(post)"
*ngFor="let post of blogPosts">
{{ post.title }}
</li>
</ul>
`,
changeDetectionStrategy: ChangeDetectionStrategy.OnPush
)
export class BlogPostListComponent {
@Input() blogPosts: BlogPost[];
@Input() selectedPost: BlogPost;
@Output() selectPost = new EventEmitter<BlogPost>();
public get numberOfBlogPosts() {
return this.blogPosts?.length ?? 0;
}
public get numberOfPublishedBlogPosts() {
return (this.blogPosts || []).filter(blogPost => blogPost.published);
}
public isSelected(post) {
this.selectedPost === post;
}
}
Code is readable, and easy to unittest.
Closing notes
Obviously this is an extremely simplified example, but believe me, as complexity grows, there are much to be gained by doing handling your observables in a consistent and safe manner from the get-go. RxJS is immensely powerful, and it's easy to abuse. With all the different possibilites at your hand, it's just one more operator in my .pipe(...)
right? Well, things quickly get out of hand, and all of a sudden you have a mess of operators and hard to follow code.
Keep it simple, refactor and decompose, and you'll be much happier when you revisit your code down the line.
Top comments (0)