DestroyRef został wprowadzony w Angular 16 (commit link) i daje nam możliwość uruchomienia callback’a, gdy komponent/dyrektywa lub powiązany injector zostanie zniszczony.
Zobaczmy prosty przykład, aby zrozumieć, jak możemy tego użyć.
Callback, gdy komponent jest niszczony
import { Component } from '@angular/core';
import { interval } from 'rxjs';
@Component({
selector: 'app-dashboard',
standalone: true,
template: ``,
})
export default class DashboardComponent {
constructor() {
interval(1000).subscribe((value) => {
console.log(value);
});
}
}
Powyższy kod emituje nową wartość co 1 sekundę (1000 ms) i wyrzuca ją do konsoli. Ten niewielki fragment kodu nadal powoduje wyciek pamięci, ponieważ nie niszczymy subskrypcji.
Odpowiedzmy na kilka pytań.
P: Co się stanie, jeśli znawigujemy się do innego route’a?
O: Cóż, komponent zostanie zniszczony.
P: Co by się stało, gdybyśmy wrócili na tę trasę?
O: Cóż, komponent zostanie skonstruowany ponownie.
Pomimo zniszczenia komponentu, subskrypcja pozostaje aktywna.
import { Component, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-dashboard',
standalone: true,
template: ``,
})
export default class DashboardComponent implements OnDestroy {
#subscription?: Subscription;
constructor() {
this.#subscription = interval(1000).subscribe((value) => {
console.log(value);
});
}
ngOnDestroy(): void {
this.#subscription?.unsubscribe();
}
}
Musimy anulować subskrypcję, aby uniknąć wycieku pamięci. Ale prawdopodobnie już o tym wiesz i stosujesz się do tego w praktyce ?
Zróbmy teraz to samo, ale tym razem używając `DestroyRef` zamiast hook’a OnDestroy
import { Component, DestroyRef, inject } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-dashboard',
standalone: true,
template: ``,
})
export default class DashboardComponent {
#subscription?: Subscription;
#destroyRef = inject(DestroyRef);
constructor() {
this.#subscription = interval(1000).subscribe((value) => {
console.log(value);
});
this.#destroyRef.onDestroy(() => {
this.#subscription?.unsubscribe();
});
}
}
Przejdźmy przez ten kod po kolei:
- Tworzymy instancję #destroyRef przy użyciu metody `inject` (należy pamiętać, że może to robić tylko wewnątrz tzw. injection context).
- Rejestrujemy callback w metodzie `onDestroy`. Podana funkcja zostanie wykonana, gdy komponent zostanie zniszczony.
Alternatywnie, moglibyśmy napisać to w ten sposób:
export default class DashboardComponent {
#subscription?: Subscription;
constructor() {
this.#subscription = interval(1000).subscribe((value) => {
console.log(value);
});
inject(DestroyRef).onDestroy(() => {
this.#subscription?.unsubscribe();
});
}
}
Zauważ, że tym razem używamy funkcji `inject` w konstruktorze. To nadal działa dobrze, ponieważ ciało konstruktora zawiera się również w injection context’cie.
Istnieje jednak lepszy sposób na zamknięcie subskrybcji, bądź cierpliwa/y! 🙂
TakeUntilDestroyed
Zanim przejdziemy do tego lepszego sposobu, zaimplementujmy customową metodę `myTakeUntilDestroyed`.
export default class DashboardComponent {
#subscription?: Subscription;
myTakeUntilDestroyed() {
inject(DestroyRef).onDestroy(() => {
this.#subscription?.unsubscribe();
});
}
constructor() {
this.#subscription = interval(1000).subscribe((value) => {
console.log(value);
});
this.myTakeUntilDestroyed();
}
}
Stworzyłem metodę myTakeUntilDestroyed
, która wstrzykuje DestroyRef
. Ważne jest, aby zrozumieć, że nie możemy użyć metody inject poza injection context’em. W powyższym przykładzie wywołuję myTakeUntilDestroyed
z konstruktora, co działa poprawnie.
Injection Context: Konstruktor, pola klasy, factory function -> Czytaj więcej
Co by się stało, gdybyśmy wywołali metodę z hook’a `ngOnInit`?
export default class DashboardComponent implements OnInit {
#subscription?: Subscription;
myTakeUntilDestroyed() {
inject(DestroyRef).onDestroy(() => {
this.#subscription?.unsubscribe();
});
}
constructor() {
this.#subscription = interval(1000).subscribe((value) => {
console.log(value);
});
}
ngOnInit(): void {
this.myTakeUntilDestroyed();
}
}
Ponieważ nie jesteśmy w injection context’cie, Angular zgłosi błąd.
Jeśli chcielibyśmy wywołać `myTakeUntilDestroyed` z hook’a `ngOnInit`, powinniśmy zmienić sposób dostępu do `DestroyRef`.
myTakeUntilDestroyed(destroyRef?: DestroyRef) {
(destroyRef ?? inject(DestroyRef)).onDestroy(() => {
this.#subscription?.unsubscribe();
});
}
Zmiana ta pozwala na użycie `myTakeUntilDestroyed` poza kontekstem wstrzykiwania, np, w hook’u OnInit.
export default class DashboardComponent implements OnInit {
#subscription?: Subscription;
#destroyRef = inject(DestroyRef);
myTakeUntilDestroyed(destroyRef?: DestroyRef) {
(destroyRef ?? inject(DestroyRef)).onDestroy(() => {
this.#subscription?.unsubscribe();
});
}
constructor() {
this.#subscription = interval(1000).subscribe((value) => {
console.log(value);
});
}
ngOnInit(): void {
this.myTakeUntilDestroyed(this.#destroyRef);
}
}
Bazując na tym czego dowiedzieliśmy się implementując metodę `myTakeUntilDestroyed` , możemy przejść do docelowego rozwiązania, tj. rxjs’owego operatora `takeUntilDestroyed`.
takeUntilDestroyed
kończy subskrybcję, gdy komponent/dyrektywa zostanie zniszczona lub gdy przekazany do niej injector zostanie zniszczony.
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export default class DashboardComponent {
constructor() {
interval(1000)
.pipe(takeUntilDestroyed())
.subscribe((value) => {
console.log(value);
});
}
}
Osiągnęliśmy to samo z czytelniejszym kodem i wykorzystaniem istniejącego operatora. Ale co jeśli chcemy go użyć w hook’u `ngOnInit`?
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export default class DashboardComponent implements OnInit {
#destroyRef = inject(DestroyRef);
ngOnInit(): void {
interval(1000)
.pipe(takeUntilDestroyed(this.#destroyRef))
.subscribe((value) => {
console.log(value);
});
}
}
Jeśli musimy użyć operatora takeUntilDestroyed
poza injection context’em, my (programiści) jesteśmy odpowiedzialni za dostarczenie `DestroyRef` jako parametru, analogiczniej jak w naszej customowej metodzie myTakeUntilDestroyed.
Jeśli lubisz oglądać filmy, koniecznie obejrzyj ten, który obejmuje przydatne informacje o DestroyRef > Obejrzyj teraz
Przydatne linki:
- Kod i dokumentacja takeUntilDestroyed
- Dokumentacja metody wstrzykiwania
- Dokumentacja DestroyRef
Dzięki za przeczytanie mojego artykułu!