05 wrz 2023
5 min

Poznaj DestroyRef!

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: 

Dzięki za przeczytanie mojego artykułu!

Podziel się artykułem

Zapisz się na nasz newsletter

Dołącz do community Angular.love i bądź na bieżąco z trendami.