Skip to content

Commit 1e8158b

Browse files
kristilwpkozlowski-opensource
authored andcommitted
fix(core): components marked for traversal resets reactive context (#61663)
when marked for traversal the reactive context has to be set to null to avoid inheriting the reactive context of the parent component PR Closes #61662 PR Close #61663
1 parent 1e4ce31 commit 1e8158b

File tree

2 files changed

+62
-11
lines changed

2 files changed

+62
-11
lines changed

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

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
consumerPollProducersForChange,
1414
getActiveConsumer,
1515
ReactiveNode,
16+
setActiveConsumer,
1617
} from '../../../primitives/signals';
1718

1819
import {RuntimeError, RuntimeErrorCode} from '../../errors';
@@ -207,7 +208,10 @@ export function refreshView<T>(
207208
} else if (getActiveConsumer() === null) {
208209
// If the current view should not have a reactive consumer but we don't have an active consumer,
209210
// we still need to create a temporary consumer to track any signal reads in this template.
210-
// This is a rare case that can happen with `viewContainerRef.createEmbeddedView(...).detectChanges()`.
211+
// This is a rare case that can happen with
212+
// - `viewContainerRef.createEmbeddedView(...).detectChanges()`.
213+
// - `viewContainerRef.createEmbeddedView(...)` without any other dirty marking on the parent,
214+
// flagging the parent component for traversal but not triggering a full `refreshView`.
211215
// This temporary consumer marks the first parent that _should_ have a consumer for refresh.
212216
// Once that refresh happens, the signals will be tracked in the parent consumer and we can destroy
213217
// the temporary one.
@@ -490,16 +494,22 @@ function detectChangesInView(lView: LView, mode: ChangeDetectionMode) {
490494
if (shouldRefreshView) {
491495
refreshView(tView, lView, tView.template, lView[CONTEXT]);
492496
} else if (flags & LViewFlags.HasChildViewsToRefresh) {
493-
if (!isInCheckNoChangesPass) {
494-
runEffectsInView(lView);
495-
}
496-
detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Targeted);
497-
const components = tView.components;
498-
if (components !== null) {
499-
detectChangesInChildComponents(lView, components, ChangeDetectionMode.Targeted);
500-
}
501-
if (!isInCheckNoChangesPass) {
502-
addAfterRenderSequencesForView(lView);
497+
// Set active consumer to null to avoid inheriting an improper reactive context
498+
const prevConsumer = setActiveConsumer(null);
499+
try {
500+
if (!isInCheckNoChangesPass) {
501+
runEffectsInView(lView);
502+
}
503+
detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Targeted);
504+
const components = tView.components;
505+
if (components !== null) {
506+
detectChangesInChildComponents(lView, components, ChangeDetectionMode.Targeted);
507+
}
508+
if (!isInCheckNoChangesPass) {
509+
addAfterRenderSequencesForView(lView);
510+
}
511+
} finally {
512+
setActiveConsumer(prevConsumer);
503513
}
504514
}
505515
}

packages/core/test/acceptance/change_detection_spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,47 @@ describe('change detection', () => {
222222

223223
expect(counter).toBe(3);
224224
});
225+
226+
it('updating signal inside an EmbeddedView in a child component with OnPush inside a parent component with Default CD', async () => {
227+
const data = signal('initial');
228+
229+
@Component({
230+
selector: 'child',
231+
template: '<ng-container *viewManipulation>{{data()}}</ng-container>',
232+
imports: [ViewManipulation],
233+
changeDetection: ChangeDetectionStrategy.OnPush,
234+
})
235+
class ChildComponent {
236+
data = data;
237+
}
238+
239+
@Component({
240+
template: '<child/>',
241+
changeDetection: ChangeDetectionStrategy.Default,
242+
imports: [ChildComponent],
243+
})
244+
class ParentComponent {}
245+
246+
TestBed.configureTestingModule({
247+
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
248+
});
249+
250+
const fixture = TestBed.createComponent(ParentComponent);
251+
await fixture.whenStable();
252+
expect(fixture.nativeElement.innerText).toBe('');
253+
254+
fixture.debugElement
255+
.queryAllNodes(By.directive(ViewManipulation))[0]
256+
.injector.get(ViewManipulation)
257+
.insertIntoVcRef();
258+
await fixture.whenStable();
259+
expect(fixture.nativeElement.innerText).toBe(data());
260+
261+
data.set('new');
262+
expect(fixture.isStable()).toBe(false);
263+
await fixture.whenStable();
264+
expect(fixture.nativeElement.innerText).toBe(data());
265+
});
225266
});
226267

227268
describe('markForCheck', () => {

0 commit comments

Comments
 (0)