Skip to content

Commit

Permalink
fix(core): cleanup signal consumers for all views (angular#53351)
Browse files Browse the repository at this point in the history
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 angular#53351
  • Loading branch information
JoostK authored and amilamen committed Jan 26, 2024
1 parent 88337a2 commit ea1cfb9
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 6 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/render3/node_manipulation.ts
Expand Up @@ -374,8 +374,6 @@ export function destroyLView(tView: TView, lView: LView) {
if (!(lView[FLAGS] & LViewFlags.Destroyed)) {
const renderer = lView[RENDERER];

lView[REACTIVE_TEMPLATE_CONSUMER] && consumerDestroy(lView[REACTIVE_TEMPLATE_CONSUMER]);

if (renderer.destroyNode) {
applyView(tView, lView, renderer, WalkTNodeTreeAction.Destroy, null, null);
}
Expand Down Expand Up @@ -405,6 +403,8 @@ function cleanUpView(tView: TView, lView: LView): void {
// really more of an "afterDestroy" hook if you think about it.
lView[FLAGS] |= LViewFlags.Destroyed;

lView[REACTIVE_TEMPLATE_CONSUMER] && consumerDestroy(lView[REACTIVE_TEMPLATE_CONSUMER]);

executeOnDestroys(tView, lView);
processCleanups(tView, lView);
// For component views only, the local renderer is destroyed at clean up time.
Expand Down
1 change: 1 addition & 0 deletions packages/core/test/acceptance/BUILD.bazel
Expand Up @@ -23,6 +23,7 @@ ts_library(
"//packages/common/locales",
"//packages/compiler",
"//packages/core",
"//packages/core/primitives/signals",
"//packages/core/src/util",
"//packages/core/test/render3:matchers",
"//packages/core/testing",
Expand Down
Expand Up @@ -9,6 +9,7 @@
import {NgFor, NgIf} from '@angular/common';
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
import {afterNextRender, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, Directive, inject, Input, PLATFORM_ID, signal, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {ReactiveNode, SIGNAL} from '@angular/core/primitives/signals';
import {TestBed} from '@angular/core/testing';

describe('CheckAlways components', () => {
Expand Down Expand Up @@ -753,10 +754,10 @@ describe('OnPush components with signals', () => {
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIf],
template: `
<div *ngIf="true">
<div *ngIf="true">
<div *ngIf="true">
{{value()}}
<div *ngIf="true">
<div *ngIf="true">
<div *ngIf="true">
{{value()}}
</div>
</div>
</div>
Expand Down Expand Up @@ -910,6 +911,53 @@ describe('OnPush components with signals', () => {
const fixture = TestBed.createComponent(TestCmp);
expect(() => fixture.detectChanges()).toThrowError(/ExpressionChanged/);
});

it('destroys all signal consumers when destroying the view tree', () => {
const val = signal(1);
const double = computed(() => val() * 2);

@Component({
template: '{{double()}}',
selector: 'child',
standalone: true,
})
class Child {
double = double;
}

@Component({
template: '|{{double()}}|<child />|',
imports: [Child],
standalone: true,
})
class SignalComponent {
double = double;
}

const fixture = TestBed.createComponent(SignalComponent);
fixture.detectChanges();
expect(fixture.nativeElement.innerText).toEqual('|2|2|');

const node = double[SIGNAL] as ReactiveNode;
expect(node.dirty).toBe(false);

// Change the signal to verify that the computed is dirtied while being read from the template.
val.set(2);
expect(node.dirty).toBe(true);
fixture.detectChanges();
expect(node.dirty).toBe(false);
expect(fixture.nativeElement.innerText).toEqual('|4|4|');

// Destroy the view tree to verify that the computed is unconnected from the graph for all
// views.
fixture.destroy();
expect(node.dirty).toBe(false);

// Writing further updates to the signal should not cause the computed to become dirty, since it
// is no longer being observed.
val.set(3);
expect(node.dirty).toBe(false);
});
});


Expand Down

0 comments on commit ea1cfb9

Please sign in to comment.