Skip to content

Commit 0a53f96

Browse files
JoostKdylhunn
authored andcommitted
fix(core): cleanup signal consumers for all views (#53351)
This commit fixes a memory leak where signal consumers would not be cleaned up for descendant views when a view is destroyed, because the cleanup logic was only invoked for the view that is itself being destroyed. PR Close #53351
1 parent dc27bea commit 0a53f96

File tree

3 files changed

+55
-6
lines changed

3 files changed

+55
-6
lines changed

packages/core/src/render3/node_manipulation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,6 @@ export function destroyLView(tView: TView, lView: LView) {
374374
if (!(lView[FLAGS] & LViewFlags.Destroyed)) {
375375
const renderer = lView[RENDERER];
376376

377-
lView[REACTIVE_TEMPLATE_CONSUMER] && consumerDestroy(lView[REACTIVE_TEMPLATE_CONSUMER]);
378-
379377
if (renderer.destroyNode) {
380378
applyView(tView, lView, renderer, WalkTNodeTreeAction.Destroy, null, null);
381379
}
@@ -405,6 +403,8 @@ function cleanUpView(tView: TView, lView: LView): void {
405403
// really more of an "afterDestroy" hook if you think about it.
406404
lView[FLAGS] |= LViewFlags.Destroyed;
407405

406+
lView[REACTIVE_TEMPLATE_CONSUMER] && consumerDestroy(lView[REACTIVE_TEMPLATE_CONSUMER]);
407+
408408
executeOnDestroys(tView, lView);
409409
processCleanups(tView, lView);
410410
// For component views only, the local renderer is destroyed at clean up time.

packages/core/test/acceptance/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ts_library(
2323
"//packages/common/locales",
2424
"//packages/compiler",
2525
"//packages/core",
26+
"//packages/core/primitives/signals",
2627
"//packages/core/src/util",
2728
"//packages/core/test/render3:matchers",
2829
"//packages/core/testing",

packages/core/test/acceptance/change_detection_signals_in_zones_spec.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {NgFor, NgIf} from '@angular/common';
1010
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
1111
import {afterNextRender, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, Directive, inject, Input, PLATFORM_ID, signal, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
12+
import {ReactiveNode, SIGNAL} from '@angular/core/primitives/signals';
1213
import {TestBed} from '@angular/core/testing';
1314

1415
describe('CheckAlways components', () => {
@@ -753,10 +754,10 @@ describe('OnPush components with signals', () => {
753754
changeDetection: ChangeDetectionStrategy.OnPush,
754755
imports: [NgIf],
755756
template: `
756-
<div *ngIf="true">
757-
<div *ngIf="true">
758-
<div *ngIf="true">
759-
{{value()}}
757+
<div *ngIf="true">
758+
<div *ngIf="true">
759+
<div *ngIf="true">
760+
{{value()}}
760761
</div>
761762
</div>
762763
</div>
@@ -910,6 +911,53 @@ describe('OnPush components with signals', () => {
910911
const fixture = TestBed.createComponent(TestCmp);
911912
expect(() => fixture.detectChanges()).toThrowError(/ExpressionChanged/);
912913
});
914+
915+
it('destroys all signal consumers when destroying the view tree', () => {
916+
const val = signal(1);
917+
const double = computed(() => val() * 2);
918+
919+
@Component({
920+
template: '{{double()}}',
921+
selector: 'child',
922+
standalone: true,
923+
})
924+
class Child {
925+
double = double;
926+
}
927+
928+
@Component({
929+
template: '|{{double()}}|<child />|',
930+
imports: [Child],
931+
standalone: true,
932+
})
933+
class SignalComponent {
934+
double = double;
935+
}
936+
937+
const fixture = TestBed.createComponent(SignalComponent);
938+
fixture.detectChanges();
939+
expect(fixture.nativeElement.innerText).toEqual('|2|2|');
940+
941+
const node = double[SIGNAL] as ReactiveNode;
942+
expect(node.dirty).toBe(false);
943+
944+
// Change the signal to verify that the computed is dirtied while being read from the template.
945+
val.set(2);
946+
expect(node.dirty).toBe(true);
947+
fixture.detectChanges();
948+
expect(node.dirty).toBe(false);
949+
expect(fixture.nativeElement.innerText).toEqual('|4|4|');
950+
951+
// Destroy the view tree to verify that the computed is unconnected from the graph for all
952+
// views.
953+
fixture.destroy();
954+
expect(node.dirty).toBe(false);
955+
956+
// Writing further updates to the signal should not cause the computed to become dirty, since it
957+
// is no longer being observed.
958+
val.set(3);
959+
expect(node.dirty).toBe(false);
960+
});
913961
});
914962

915963

0 commit comments

Comments
 (0)