Reintroduce NgRx - Observable (part 1)
NgRx has been around as long as Angular. It is a very popular and so highly praised, if you don’t use it, you will feel you are missed out. NgRx is also controversial. There are lots of articles advocating NgRx or asking people stop using it. I have used it for a long time in various projects, and I have seen good code and bad code in using NgRx. In the post, I would like reintroduce NgRx from a new perspective. All the source code of the post can be found here. The live demo is here
NgRx is a framework for building reactive applications in Angular.
What does reactive
mean? It deals with the propagation of change, which can
occur in many forms such as user inputs, sensor outputs, or data updates from a
server. The core idea behind reactive programming is to model these changes as
observable data streams that can be manipulated, transformed, and reacted upon.
The main concept is observable.
Why use observable in Angular?
NgRx use observable to implement this reactivity. But doesn’t Angular support reactivity out of the box? Angular’s two-way binding support updating view when state changes, and updating state when when view changes. Angular’s default change detection doesn’t require you to use Observable, in fact you can put state in any data structure.
This is how it works. When a variable in a primitive data type such as number
changes, it does not triggered any event for UI to react. Angular uses ZoneJs
fake the reactivity. When data is about changed, angular can seize the moment,
setup a hook, let the your code run, after that the hook will do a
undiscriminating change detection. Your code maybe or maybe not change a state,
Angular just run change detection anyway. This is angular’s default change
detection strategy.
This seems inefficient. But in reality, this works fine in a lot of case. But if
you can have tens of thousands of component instance in the page, performance
can be slow. To improve performance, Angular uses another change detection
strategy OnPush
. In this strategy, the change to @Input
property of a
component can automatic trigger change detection of the component and its
children. If other state of the component or state outside of the component is
changed, a component will not do change detection, and you have to explicitly
call changeDetectorRef.markForCheck()
for the component. This can reduce
the number of change detection, so that performance can be improved.
In the following, I will only use OnPush
component to illustrate the
problem, why and how Observable can solve this problem. These problem does not
exists for component using default change detection strategy.
State in primitive type
In the following example, two OnPush
components shared a service with state in
number type. When the state is changed in counter 1 component, the counter 2
component can’t not detect the change because it is OnPush
component.
Here is the source code.
@Injectable({
providedIn: "root",
})
export class SimpleCounterService {
get result() {
return this._result;
}
private _result = 0;
add() {
this._result++;
}
}
@Component({
selector: "app-simple-counter",
template: `
{{ logId() }}
<h1>buggy simple counter: {{ id }}</h1>
<h2>Result: {{ result }}</h2>
<button (click)="add()">Add</button>
`,
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SimpleCounterComponent extends Debug {
constructor(private simpleService: SimpleCounterService) {
super();
}
get result() {
return this.simpleService.result;
}
add() {
this.simpleService.add();
}
}
State in BehaviorSubject
Observable is reactive. After you subscribe to it, when it emits new value, you
can be notified. In the follow example, I changed the state to BehaviorSubject
observable, and manually subscribe to the observable like the following, but it
is still buggy. The one component still can not show the state updated in
another component.
@Injectable({
providedIn: "root",
})
export class BehaviorCounterService {
private subject = new BehaviorSubject(0);
result$ = this.subject.asObservable();
add() {
this.subject.next(this.subject.value + 1);
}
}
@Component({
selector: "app-behavior-subject-counter-with-bug",
template: `
{{ logId() }}
<h1>buggy behavior subject counter: {{ id }}</h1>
<h2>Result: {{ result }}</h2>
<button (click)="add()">Add</button>
`,
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BehaviorSubjectCounterWithBugComponent extends Debug {
constructor(private rxjsService: BehaviorCounterService) {
super();
this.rxjsService.result$.subscribe((result) => {
this.result = result;
//call changeDetectorRef.markForCheck() here, it can fix the bug
});
}
result!: number;
add() {
this.rxjsService.add();
}
}
The manual subscription like this works in default change detection strategy.
But not here. The problem is the same as previous example, it is OnPush
component, you have to tell angular explicitly the component is dirty, by
calling changeDetectorRef.markForCheck()
, even your state is changed in
subscription callback. The following is the quick fix.
constructor(private rxjsService: BehaviorCounterService, changeDetector : ChangeDetectorRef) {
super();
this.rxjsService.result$.subscribe((result) => {
this.result = result;
changeDetector.markForCheck();
});
}
Ok. Observable does not automatically solve the change detection problem.
In the following, instead of manually subscribing to the observable, I use
async
pipeline in the template. The pipeline not only subscribe to the
observable, it also mark the component dirty by calling calling
changeDetectorRef.markForCheck()
. Even more, it automatically unsubscribes the
observable when the component is destroyed. Now the code is not buggy
any more. It is better to use observable, async
pipe, and OnPush
together. So refrain from subscribing to observable manually.
@Component({
selector: "app-behavior-subject-counter",
template: `
{{ logId() }}
<h1>behavior subject counter: {{ id }}</h1>
<h2>Result: {{ result$ | async }}</h2>
<button (click)="add()">Add</button>
`,
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BehaviorSubjectCounterComponent extends Debug {
constructor(private rxjsService: BehaviorCounterService) {
super();
}
result$ = this.rxjsService.result$;
add() {
this.rxjsService.add();
}
}
Using observable not only solves the angular performance issue, but it also create new paradigm of programming. It is a pushed-based architecture. You no longer need to use do change detection in a pull style. Instead, after the state is updated anywhere in the application, the state will notify you if you subscribe it.