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.