Reputation: 629
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
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
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
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:
<app-child value={{sharedService.value}}>
<label *ngIf=value$ | async as value>child shared service value: {{value}}</label>
<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