If you're a Medium Member, please read it there
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 there are very few Signal leaks out there in the wild if any.
// 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);
}
updateClicks() {
this.counter.update((v) => v + 1);
}
// easier to spot in memory if it's huge
private marker = new Array(500000).fill(1000);
private perpetrator = signal(1);
private counter = signal(0);
}
We are counting clicks conditionally. If there were no conditions there’d be no need for computed . But! We also want to show/carry the click count to other components, so we store it in a service. We could call the service every time manually whenever we know the click count would change, but we have a nice Signal for it, why not let value changes get detected automatically? That’s what Signals are made for.
import {
computed,
Injectable,
signal,
Signal,
WritableSignal,
} from '@angular/core';
@Injectable({ providedIn: 'root' })
export class RootService {
private cSignal: Signal<number> | undefined;
private nSignal: WritableSignal<number> = signal(0);
private updaterSignalCache: Signal<number> | undefined;
getCSignal(): Signal<number> | undefined {
return this.cSignal;
}
setUpdater(updaterSignal: Signal<number>): Signal<number> {
if (this.updaterSignalCache === updaterSignal) {
return this.cSignal!;
}
this.updaterSignalCache = updaterSignal;
this.cSignal = computed(() => {
return this.nSignal() + this.updaterSignalCache!();
});
return this.cSignal;
}
}
The service also does its computation and adjusts the number according to some configuration, not a groundbreaking concept.
And if you add a second component, it’ll work just as expected and designed.
@Component({
selector: 'app-second',
standalone: true,
imports: [RouterModule],
template: `<a routerLink="..">TO First component</a>
<br />
{{service.getCSignal?.()}} `,
})
export class SecondComponent {
service = inject(RootService);
}
And since the service’s computeddepends on FirstComponent’s computed , which in turn refers tothis , this being FirstComponent, it’ll retain the component in memory.

Takeaway (considering only memory leaks)
saving a Signal in a Service, no problems with that ✅
creating a computed Signal in a service from a Component’s Signals will automatically update the Service’s value ✅ (you’ll retain the computed value after the Component dies, but cannot update it anymore as the dependent Signals’ setter is deleted with the Component unless you save them as well)
computed Signals in Components mostly refer the Component, do not save them outside the Component, especially in long-lived Services ❌
functions retain context, so if you save the function, you save everything it refers! ❌
Comentários