Member-only story
The simplest way to leak memory with Angular Signals
It’s Signals, so you need to make an effort for this to happen.
The basics are very straightforward, but Signals being relatively new, not many people had this idea yet, so you’ll encounter very few Signal leaks out there in the wild if any.
Read the non-Member version here
// Component
item = { name: 'john', log: () => this.log('logger called')};
updateOnClick() {
this.service.updateList(this.item);
}
log(a: any) {
console.log(a);
}
// Service (provided in 'root')
list = signal<any[]>([]);
total = computed(() => this.list().length);
updateList(item: { name: string }) {
this.list.update((l) => [item, ...l]);
}
We save an object into a long lived service, and that object refers to the component from one of its property functions. Through the function context (closure’s lexical environment), the Component will also be retained for as long as the list has the item.
Not to pick on anyone, just pointing randomly at a live example GitHub gave when I searched for signals, this one already happens in projects. The logic is the same, there’s an object with onConfirm
callback that uses this
, saved in a Signal, and never released due to a minor coding error. This one
could happen to anyone and does not have to be Signal to make it happen.
It is an important example though as it highlights how saving functions means also saving context, and that it can cause issues fast. With computed
Signals we store both a value, dependencies, AND also a computation
, a function that is. Knowing that, let’s break out of GC’s tyranny!
import { Component, computed, inject, Signal, signal } from '@angular/core';
import { RouterModule } from '@angular/router';
import { RootService } from '../services/root.service';
@Component({
selector: 'app-first',
standalone: true,
imports: [RouterModule],
template: `<a routerLink="/second" id="link">TO Second component</a> <br />
<button (click)="updateClicks()">click if you dare</button>
<br />
Combined {{ cSignal?.() }}`,
})
export class FirstComponent {
clicks = computed(() => {
if (this.perpetrator() > 1) {
return this.counter();
}
return this.counter() + 1;
});
service = inject(RootService);
cSignal: Signal<number> | undefined;
constructor() {
this.cSignal = this.service.setUpdater(this.clicks);
}…