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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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.
1 2 3 4 5 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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.
1 2 3 4 5 6 7 8 9 10 11 |
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?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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!
Dodaj komentarz