Skip to content

Commit 40bb45f

Browse files
atscottthePunderWoman
authored andcommitted
fix(core): Respect OnPush change detection strategy for dynamically created components (#51356)
This commit fixes a bug in the change detection algorithm that would ignore the `OnPush`/dirty flag of a component's host when it is created dynamically. That is, `OnPush` components that were not marked dirty but were created as embedded views would have their host bindings and `ngDoCheck` function always run even if they were not dirty. BREAKING CHANGE: `OnPush` components that are created dynamically now only have their host bindings refreshed and `ngDoCheck run` during change detection if they are dirty. Previously, a bug in the change detection would result in the `OnPush` configuration of dynamically created components to be ignored when executing host bindings and the `ngDoCheck` function. This is rarely encountered but can happen if code has a handle on the `ComponentRef` instance and updates values read in the `OnPush` component template without then calling either `markForCheck` or `detectChanges` on that component's `ChangeDetectorRef`. PR Close #51356
1 parent b37ba05 commit 40bb45f

File tree

2 files changed

+36
-16
lines changed

2 files changed

+36
-16
lines changed

packages/core/src/render3/instructions/change_detection.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,6 @@ const enum ChangeDetectionMode {
9696
* flag are refreshed.
9797
*/
9898
Targeted,
99-
/**
100-
* Used when refreshing a view to force a refresh of its embedded views. This mode
101-
* refreshes views without taking into account their LView flags, i.e. non-dirty OnPush components
102-
* will be refreshed in this mode.
103-
*
104-
* TODO: we should work to remove this mode. It's used in `refreshView` because that's how the
105-
* code worked before introducing ChangeDetectionMode. Instead, it should pass `Global` to the
106-
* `detectChangesInEmbeddedViews`. We should aim to fix this by v17 or, at the very least, prevent
107-
* this flag from affecting signal views not specifically marked for refresh (currently, this flag
108-
* would _also_ force signal views to be refreshed).
109-
*/
110-
BugToForceRefreshAndIgnoreViewFlags
11199
}
112100

113101
/**
@@ -164,7 +152,7 @@ export function refreshView<T>(
164152
// insertion points. This is needed to avoid the situation where the template is defined in this
165153
// `LView` but its declaration appears after the insertion component.
166154
markTransplantedViewsForRefresh(lView);
167-
detectChangesInEmbeddedViews(lView, ChangeDetectionMode.BugToForceRefreshAndIgnoreViewFlags);
155+
detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Global);
168156

169157
// Content query results must be refreshed before content hooks are called.
170158
if (tView.contentQueries !== null) {
@@ -316,8 +304,7 @@ function detectChangesInView(lView: LView, mode: ChangeDetectionMode) {
316304
const flags = lView[FLAGS];
317305
if ((flags & (LViewFlags.CheckAlways | LViewFlags.Dirty) &&
318306
mode === ChangeDetectionMode.Global) ||
319-
flags & LViewFlags.RefreshView ||
320-
mode === ChangeDetectionMode.BugToForceRefreshAndIgnoreViewFlags) {
307+
flags & LViewFlags.RefreshView) {
321308
refreshView(tView, lView, tView.template, lView[CONTEXT]);
322309
} else if (lView[DESCENDANT_VIEWS_TO_REFRESH] > 0) {
323310
detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Targeted);

packages/core/test/acceptance/change_detection_spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
import {CommonModule} from '@angular/common';
11-
import {ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, Directive, DoCheck, EmbeddedViewRef, ErrorHandler, EventEmitter, Input, NgModule, OnInit, Output, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
11+
import {ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, createComponent, Directive, DoCheck, EmbeddedViewRef, ErrorHandler, EventEmitter, inject, Input, NgModule, OnInit, Output, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
1212
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
1313
import {expect} from '@angular/platform-browser/testing/src/matchers';
1414
import {BehaviorSubject} from 'rxjs';
@@ -66,6 +66,39 @@ describe('change detection', () => {
6666
expect(viewRef.rootNodes[0]).toHaveText('change-detected');
6767
});
6868

69+
it('should not detect changes for OnPush embedded views when they are not dirty', () => {
70+
@Component({
71+
selector: 'onpush',
72+
template: '',
73+
standalone: true,
74+
changeDetection: ChangeDetectionStrategy.OnPush
75+
})
76+
class OnPushComponent {
77+
checks = 0;
78+
cdRef = inject(ChangeDetectorRef);
79+
ngDoCheck() {
80+
this.checks++;
81+
}
82+
}
83+
84+
@Component({template: '<ng-template #template></ng-template>', standalone: true})
85+
class Container {
86+
@ViewChild('template', {read: ViewContainerRef, static: true}) vcr!: ViewContainerRef;
87+
}
88+
const fixture = TestBed.createComponent(Container);
89+
const ref = fixture.componentInstance.vcr!.createComponent(OnPushComponent);
90+
91+
fixture.detectChanges(false);
92+
expect(ref.instance.checks).toBe(1);
93+
94+
fixture.detectChanges(false);
95+
expect(ref.instance.checks).toBe(1);
96+
97+
ref.instance.cdRef.markForCheck();
98+
fixture.detectChanges(false);
99+
expect(ref.instance.checks).toBe(2);
100+
});
101+
69102
it('should not detect changes in child embedded views while they are detached', () => {
70103
const counters = {componentView: 0, embeddedView: 0};
71104

0 commit comments

Comments
 (0)