No accurate documentation on angular change detection

597 Views Asked by At

Recently I've been learning and experimenting with angular change detection. What I found out is that it's much different than it's described in most articles and documentation, especially regarding onPush components.

Example of behaviour that I didn't find described anywhere: Let's say we have component detached from change detection, and it has onPush child.

  • When we manually trigger change detection in parent, child will remain unchanged.
  • When we fire some event in child, change detection will not be triggered there either, because change detection propagation goes from top to bottom, and is stopped on detached parent.
  • However, when we first fire some event in child, and then run detectChanges() in parent, child will run change detection.

Here's example: https://stackblitz.com/edit/angular-pj9bun?file=src%2Fapp%2Fparent%2Fparent.component.ts

For me it looks like there are 2 phases. First, components are marked for change. Starting from component that fired event, going to root, and propagating to all non onPush components. Then, real change detection occurs, starting from root, propagating through all components that are marked for change. This would explain why OnPush component form my example only had change detection run when I clicked its button before-hands. It also seems plausible because even changeDetectorRef has method markForCheck() that does something similar.

But this is just my theory, I didn't find it spcecified in any documentation, or even article. Is there something like that? This would help me better understand how change detection works overall

2

There are 2 best solutions below

0
On

The calls to detach() and detectChanges() in the parent will not work in the child unless the child uses one of the methods below:

  1. @Input() changes from parent to child. If you made an @Input() value on the child and called it from parent like this: <app-child value={{sharedService.value}}>
    then you don't need the ChangeDetectorRef.
  2. If the child is bound to an observable via async pipe, then OnPush will check for that in children without needing ChangeDetectorRef <label *ngIf=value$ | async as value>child shared service value: {{value}}</label>
  1. For signals (I think Andrew was referring to). https://medium.com/ngconf/future-of-change-detection-in-angular-with-signals-fb367b66a232
  2. Event handlers also trigger the check. The set random number button is checking the children but the expression <label>child shared service value: {{sharedService.value}}</label> is not a direct descendent of the parent component and doesn't implement any of the above steps so its not marked.

In the example provided, you would need to monitor the sharedService and call ChangeDetectorRef from the child itself to force a detach/update, so you might as well make value an observable and bind to the observable via async pipe (#2 above).

0
On

You are correct as far as I know. Components are "marked", which tells the change detector to check all variables for possible UI updates. As for when exactly components get marked is an implementation detail that you'd probably have to dig through the source code to find.

The default behaviour is to always check all components on events, so all components are always marked. OnPush just changes the conditions for a component to get marked.

From: https://angular.io/guide/change-detection-skipping-subtrees#using-onpush

OnPush change detection instructs Angular to run change detection for a component subtree only when:

  • The root component of the subtree receives new inputs as the result of a template binding. Angular compares the current and past value of the input with ==
  • Angular handles an event (for example using event binding, output binding, or @HostListener ) in the subtree's root component or any of its children whether they are using OnPush change detection or not.

I think the first sentence is a bit misleading, it makes it sound like change detection starts at the subtree. Really it just marks the subtree to be updated when the change detector reaches it.

On clicking the button in the child, the second condition is satisfied, so this component is marked. However, change detection does not reach it. This is because events like the click event begin change detection at the root component, and it stops at the detached parent.

This explains why the click in the child component updates its UI on the next call of detectChanges() in the parent.

You can also provide your own logic to decide whether to mark a component for UI updates using ngDoCheck and markForCheck(). For example, to make the child always update when the parent runs change detection:

export class ChildComponent implements OnInit, DoCheck {
  constructor(
    public sharedService: SharedService,
    private ref: ChangeDetectorRef
  ) {}

  ngDoCheck() {
    this.ref.markForCheck();
  }

  ngOnInit() {}
  doNothing() {}
}

Stackblitz: https://stackblitz.com/edit/angular-pj9bun-pxjxni?file=src%2Fapp%2Fchild%2Fchild.component.ts

This is only to show you how it works, marking the component every time defeats the purpose of the OnPush strategy. You'd normally have a condition that checks if the UI needs to be updated.


The proper way to handle this pattern would be to have an observable in the service:

export class SharedService {
  public value = new BehaviorSubject(0);

  public setValue(value: number) {
    this.value.next(value);
  }
}

Use the async pipe in html

{{ sharedService.value | async }}

Then call detect changes on init, and after making changes

export class ParentComponent {
  constructor(
    public sharedService: SharedService,
    private ref: ChangeDetectorRef
  ) {
    ref.detach();
  }

  ngOnInit() {
    this.ref.detectChanges();
  }

  setRandomNumber() {
    this.sharedService.setValue(Math.random() * 100);
    this.ref.detectChanges();
  }
}

The async pipe marks the component when the value changes.

Stackblitz: https://stackblitz.com/edit/angular-pj9bun-yvkacd?file=src%2Fapp%2Fshared.service.ts