Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Angular trigger ChangeDetection on parent components on DOM events despite OnPush strategy?

Trick question I haven't found any answer in the documentation yet.

A change detection cycle is triggered when AppRef.tick() is beeing called (mostely by the NgZone patches). It goes from the root component at the top of the tree to the bottom passing by :

  • every components with Default strategy
  • every components marked by markForCheck (and their ancestors)

And skipping non-marked OnPush components.

But why, when a event is fired by a component, all its ancestors will also be checked ? As if markForCheck were called.

I was expecting the same behavior as when AppRef.tick is called. It doesn't make sense to me why OnPush parents are being checked. That behavior can well be seen on this demo.

Please also see a working illustration on stackblitz

like image 885
Matthieu Riegler Avatar asked Sep 16 '25 08:09

Matthieu Riegler


2 Answers

I needed to dive deep into the code of Angular to understand what was happening.

Angular wraps every event listener using wrapListener(). This method is responsible for calling markViewDirty when an event is fired.

This particular method markViewDirty, is also the one called by markForCheck.

So this means, every event within the inner-zone is expected to trigger CD on parent components (marked as dirty + tick() triggered).

like image 195
Matthieu Riegler Avatar answered Sep 18 '25 01:09

Matthieu Riegler


Maybe you have the same misunderstanding about OnPush that I had when I started learning about change detection :). The point is: the OnPush strategy does not change anything about which events trigger a CD cycle. This is solely taken care of by zone.js by patching browser APIs. (Unless CD is triggered manually.) zone.js does not care about components and ChangeDetectionStrategies. And CD always starts at the root as @Chris Hamilton explained.

The difference in ChangeDetectionStrategy.Default and OnPush is only if the component will actually be CHECKED during a CD cycle.

So the sentence in your original post should rather be: "OnPush strategy is known to only mark a component to be checked in the following cases".

See below an example that I observed with the profiler of the Angular DevTools. I put a setTimeout(() => {}, 6000) in the ngOnInit of the OnPush component, it triggered a CD cycle (the source is "setTimeout"), but it did not cause the component itself to be checked.

enter image description here

About the differences between manually triggered detectChanges(), tick() and markForCheck():

  • ChangeDetectorReference.detectChanges() triggers CD on this component and its children by respecting the CD strategy of the children
  • ApplicationRef.tick() triggers CD for the whole application by respecting CD strategies
  • ChangeDetectorReference.markForCheck() does NOT trigger CD, but marks all OnPush parents to be checked once in the current or next CD cycle

(well explained here)

=== EDIT: answer to the updated question ===

If a browser event listener is triggered in an OnPush component, this leads to the OnPush component being marked for check. Also, it triggers a change detection, that starts at the root of the component.

Your question, if I understand you correctly: Why do all parents of the OnPush component have to be marked for check as well?

Answer: If an OnPush component is not marked for check, not only this component will be skipped, but the whole subtree below this component will be skipped. This means, if the component where the event handler was triggered had a parent with OnPush that would not be checked, it itself would also not be checked. Or, put the other way around, if an component shall be checked, all of it ancestor also have to be checked.

like image 21
slim Avatar answered Sep 18 '25 01:09

slim