M G
M G

Reputation: 629

No accurate documentation on angular change detection

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.

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

Upvotes: 0

Views: 2359

Answers (3)

hahukap
hahukap

Reputation: 1

I have a guess that may explain all the cases: By default, change detection is triggered from the root component and traverses all child components. Unless a component calls the detach() method to detach itself, change detection will not continue to traverse it and its child components. In the case of manually triggering change detection (by injecting ChangeDetectorRef into the component and calling detectChanges()), change detection will start from the current component and traverse from there.

While traversing each component, for components using the default change detection strategy, it will directly trigger a UI update. For components using the OnPush change detection strategy, it will check whether the component has been marked for UI update, which we can call the 'flag' (the component will be marked as flag = true when one of the following conditions is met: a. event triggers; b. input property changes; c. Observable bound via async pipe calls the next method). If the condition is satisfied, the UI update will be triggered. After the UI update is complete, the flag will be set to false.

Upvotes: 0

Chris Hamilton
Chris Hamilton

Reputation: 10994

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

Upvotes: 1

iProgramGUIs
iProgramGUIs

Reputation: 164

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).

Upvotes: 1

Related Questions