Table of contents
Some great tips from John Papa and why you should (not) follow them:
RxJS Subscriptions
The first one is from a 2019 ng-conf talk where he suggests that one should unsubscribe from all our subscriptions reliably once we are done with them, typically in ngOnDestroy
.
It would be unfortunate to need to install his proposed / friend's subsink
library just so you can add all subscriptions to it and then unsubscribe from all of the using a single unsubscribe to the sink. RxJS Subscription
allows you to do this natively. Here's a code snippet from the Firebase RxJS guide:
import { interval } from 'rxjs';
const observable1 = interval(400);
const observable2 = interval(300);
const subscription = observable1.subscribe(x => console.log('first: ' + x));
const childSubscription = observable2.subscribe(x => console.log('second: ' + x));
subscription.add(childSubscription);
setTimeout(() => {
// Unsubscribes BOTH subscription and childSubscription
subscription.unsubscribe();
}, 1000);
You can also abstract away this repeated unsubscribe
in ngOnDestroy
business as mentioned by Wojciech Trawiński here:
import { OnDestroy } from "@angular/core";
import { Subscription, Observable, PartialObserver } from "rxjs";
export abstract class SelfUnsubable implements OnDestroy {
private subSink = new Subscription();
ngOnDestroy() {
this.subSink.unsubscribe();
}
protected subscribe<T>(
observable: Observable<T>,
observerOrNext?: PartialObserver<T> | ((value: T) => void),
error?: (error: any) => void,
complete?: () => void
): Subscription {
return this.subSink.add(
observable.subscribe(observerOrNext as any, error, complete)
);
}
protected unsubscribe(innerSub: Subscription) {
this.subSink.remove(innerSub);
innerSub.unsubscribe();
}
}
Now you have a less error-prone way of managing subscriptions without any external library and just good ol' OOP.
import { Component, Input, OnInit } from "@angular/core";
import { interval, Subscription } from "rxjs";
import { SelfUnsubable } from "./selfUnsubable";
@Component({
selector: "counter",
template: `
<div>Count {{ count }}</div>
`
})
export class CounterComponent extends SelfUnsubable {
count = 0;
private manualSub: Subscription;
ngOnInit() {
// unsubscribed manually
this.manualSub = this.subscribe(interval(500), value => {
this.count++;
console.log(value);
});
// unsubscribed when component is destroyed
this.subscribe(interval(1000), value => {
this.count++;
console.log(value);
});
setTimeout(this.unSubManually, 600);
}
unSubManually() {
this.unsubscribe(this.manualSub);
}
}
But wait, there's more. We can still do better.
Try your best not to subscribe in the first place!
Why is it that you are introducing imperative programming by subscribing to your reactive streams in the first place? The async
pipe is there to handle exactly this problem. You can even use OnPush
change detection strategy if you are just putting the stream directly into your components. This is much more effective than relying on zone.js based default change detection strategy. Here are two helpful talks on this topic:
NgRx Data
As NgRx developers are cautious to point out, you do not usually need NgRx. By this they usually mean that your app probably does not have use cases to justify the hundreds of lines of code bloat that NgRx requires for each of your state models.
A neat boilerplate code reduction can be achieved by @ngrx/data
. If you are already using NgRX, it goes without saying that you should at least consider using this library to significantly reduce your store related code's size. However, if you are not already using NgRX, should you use it now that @ngrx/data
is there to simplify things for you?
I personally favor using a full-fledged client-side immutable database instead of some immutable'store'. This has become easier than ever since PouchDB showed up and brought sanity to in-browser database-like solutions. Do you really need all the indirections, actions, effects, reducers, dispatchers, and selectors for each of your models? Most, if not all, of the time, you just need a basic reading of pure functional programming practices and an actual database with immutability or append-only behavior. Also note that actions, effects, reducers, dispatchers, and selectors can be thought of as general coding patterns instead of NgRX exclusive ideas.
So, is NgRx completely avoidable for even the most complex state management use cases? Your mileage may vary with the Couch-Pouch-like solution for state management. It even allows for syncing the current state to a remote server, possibly running async analytics, e.g., on user behavior, with very few additional lines of code. There is a lot more that you usually need to do with your application state than just put it in a 'store' manager. Keep it all simple by just using a database and immutable data structures.
You can use PouchDB even with databases with topic-wise event hooks like the Realtime and Firestore databases from Firebase or with web APIs with webhooks or push / pub-sub semantics. In any case, complex UI states can be easily stored and managed in client side in-memory stores or browser-based databases with or without state management solutions, even with boilerplate reducing solutions like NgRx Data, NgXS or Akita.
One particularly remarkable use case of NgRx is the possibility to create truly reactive updates to Angular without using zone.js. Here is an excellent talk from Mike Ryan regarding this exciting possibility.
More
Best practices and patterns for complicated software from big companies are not always the best for simpler projects. I will add more such (anti)-patterns here.