Kurs Angular · Moduł 2: Komponenty

Lifecycle hooks - rytuały wojownika

7 min czytania ·
W tej lekcji6

Wyobraź sobie komponent stopera, który odlicza sekundy treningu. Kiedy powinien zacząć liczyć? Kiedy pobrać z serwera dane wojownika, którego ma pokazać? I co się stanie, gdy uczeń przejdzie do innego widoku, a stoper zniknie z ekranu? Jeśli nikt go nie zatrzyma, będzie dalej tykał w pamięci przeglądarki jak duch wojownika, który macha mieczem w pustym dojo. Taki błąd nazywa się wyciekiem pamięci.

Każdy komponent Angulara przechodzi ten sam cykl życia: Angular go tworzy, przekazuje mu dane, wyświetla jego widok, odświeża go po zmianach, a na końcu usuwa. W ważnych momentach tego cyklu Angular wywołuje metody o ustalonych nazwach, zwane lifecycle hooks, czyli hookami cyklu życia. To jak rytuały samuraja: jeden przy wejściu do dojo, inny przed walką, jeszcze inny przy odejściu.

Konstruktor czy ngOnInit

Pierwszym momentem życia komponentu jest konstruktor. Angular wywołuje go, tworząc instancję klasy, i jest to chwila na przygotowanie ekwipunku, czyli na wstrzyknięcie zależności, takich jak serwisy z piątego modułu. Na tym etapie komponent nie zna jeszcze danych od rodzica. Pole oznaczone dekoratorem @Input(), którego wartość podaje rodzic w szablonie (dokładnie omówimy je w następnej lekcji), ma wtedy tylko wartość domyślną.

Dane od rodzica są gotowe dopiero w ngOnInit. Angular wywołuje ten hook raz, po ustawieniu pierwszych wartości inputów, dlatego to właściwe miejsce na logikę startową i pobieranie danych z API. Różnicę widać w tym przykładzie:

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-samurai',
  standalone: true,
  template: `<p>{{ greeting }}</p>`
})
export class SamuraiComponent implements OnInit {
  @Input() name = '';
  greeting = '';

  constructor() {
    console.log('Konstruktor, name:', this.name);
  }

  ngOnInit(): void {
    this.greeting = `Witaj, ${this.name}!`;
  }
}

Gdy rodzic napisze <app-samurai name="Musashi">, konstruktor wypisze w konsoli puste imię, bo rodzic jeszcze go nie przekazał. Zanim Angular wywoła ngOnInit, pole name ma już wartość "Musashi", więc powitanie będzie poprawne. Gdybyś zbudował je w konstruktorze, uczeń zobaczyłby "Witaj, !". Moja rada: konstruktor zostaw na zależności, a wszystko, co korzysta z danych od rodzica albo z serwera, przenieś do ngOnInit.

Interfejsy OnInit i OnDestroy

W przykładzie klasa ma dopisek implements OnInit, a OnInit pojawia się w imporcie z @angular/core. Interfejs OnInit to umowa: klasa, która go implementuje, obiecuje mieć metodę ngOnInit(). Słowo implements zapisujesz w deklaracji klasy, po jej nazwie, na przykład export class DojoComponent implements OnInit {. Obietnicę sprawdza TypeScript, co widać przy literówce w nazwie metody:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-dojo',
  standalone: true,
  template: `<h1>{{ message }}</h1>`
})
export class DojoComponent implements OnInit {
  message = '';

  // literówka: ngOnInt zamiast ngOnInit
  ngOnInt(): void {
    this.message = 'Dojo otwarte!';
  }
}

Kompilator odrzuci ten kod z komunikatem, że klasa DojoComponent niepoprawnie implementuje interfejs OnInit, bo brakuje jej metody ngOnInit. Bez dopisku implements kod skompilowałby się bez błędu, a Angular po cichu nigdy nie wywołałby metody o złej nazwie, bo hooki wywołuje po nazwie. Wiadomość "Dojo otwarte!" nigdy by się nie pojawiła, a Ty szukałbyś przyczyny godzinami. Dlatego zawsze dopisuj implements z odpowiednim interfejsem: kosztuje jedno słowo, a łapie literówki, zanim trafią do ucznia.

Każdy hook ma swój interfejs o nazwie bez przedrostka ng: OnInit dla ngOnInit, OnDestroy dla ngOnDestroy, OnChanges dla ngOnChanges. Wszystkie importujesz z @angular/core, a klasa może implementować kilka interfejsów naraz, rozdzielonych przecinkami: implements OnInit, OnDestroy.

ngOnChanges - gdy rodzic zmienia rozkaz

Hook ngOnChanges reaguje na zmiany inputów. Angular wywołuje go przed ngOnInit, jeśli komponent ma inputy powiązane w szablonie rodzica, a potem przy każdej zmianie ich wartości. Hook dostaje obiekt typu SimpleChanges, w którym pod nazwą każdego zmienionego inputu czeka opis zmiany:

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-health-bar',
  standalone: true,
  template: `<p>Zdrowie: {{ health }}</p>`
})
export class HealthBarComponent implements OnChanges {
  @Input() health = 100;

  ngOnChanges(changes: SimpleChanges): void {
    const change = changes['health'];
    if (change && !change.firstChange) {
      console.log(`Zdrowie: ${change.previousValue} -> ${change.currentValue}`);
    }
  }
}

Opis zmiany ma trzy pola: previousValue to poprzednia wartość, currentValue to nowa, a firstChange ma wartość true przy pierwszym wywołaniu, gdy rodzic dopiero ustawia input. Dlatego warunek !change.firstChange pomija start i wypisuje tylko prawdziwe zmiany, na przykład "Zdrowie: 100 -> 70" po trafieniu. Pamiętaj, że ngOnChanges reaguje tylko na nowe wartości od rodzica. Jeśli komponent sam przypisze this.health = 50, hook się nie wywoła.

ngAfterViewInit - widok gotowy do walki

Po ngOnInit Angular wyświetla szablon komponentu i komponenty dzieci. Gdy cały widok jest gotowy, wywołuje ngAfterViewInit. To pierwszy moment, w którym możesz bezpiecznie sięgnąć do elementów szablonu, na przykład przez dekorator @ViewChild, który wskazuje element oznaczony w szablonie nazwą z krzyżykiem, taką jak #nameField. Przy domyślnych ustawieniach w ngOnInit takiego elementu jeszcze nie ma. Typowe zastosowania to ustawienie kursora w polu formularza albo uruchomienie biblioteki, która rysuje wykres w gotowym elemencie. W szóstym module poznasz nowszą, sygnałową wersję tego dekoratora, czyli funkcję viewChild().

ngOnDestroy - sprzątanie przed odejściem

Ostatni rytuał to ngOnDestroy. Angular wywołuje go tuż przed usunięciem komponentu, na przykład gdy uczeń przechodzi do innego widoku albo gdy blok @if usuwa komponent ze strony. To miejsce na sprzątanie: zatrzymanie timerów, anulowanie subskrypcji i odpięcie nasłuchiwania, które komponent sam uruchomił. Oto stoper, który nie zostawia po sobie ducha:

import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `<p>Czas: {{ seconds }} s</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
  seconds = 0;
  private intervalId?: number;

  ngOnInit(): void {
    this.intervalId = window.setInterval(() => {
      this.seconds++;
    }, 1000);
  }

  ngOnDestroy(): void {
    window.clearInterval(this.intervalId);
  }
}

W ngOnInit stoper uruchamia setInterval, który co sekundę zwiększa seconds, i zapamiętuje jego identyfikator w polu intervalId. W ngOnDestroy przekazuje ten identyfikator do clearInterval, więc interwał przestaje działać razem z komponentem. Bez tej jednej linii każde wejście do widoku uruchamiałoby kolejny niewidzialny stoper, a aplikacja zwalniałaby z każdą minutą. Subskrypcje strumieni RxJS, które poznasz w siódmym module, sprząta się w tym samym miejscu.

Nowoczesny Angular oferuje też krótszą drogę. Funkcja inject() pobiera zależność prosto w polu klasy, a obiekt DestroyRef pozwala zapisać sprzątanie od razu przy kodzie, który coś uruchamia:

import { Component, DestroyRef, OnInit, inject } from '@angular/core';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `<p>Czas: {{ seconds }} s</p>`
})
export class TimerComponent implements OnInit {
  seconds = 0;
  private destroyRef = inject(DestroyRef);

  ngOnInit(): void {
    const id = window.setInterval(() => this.seconds++, 1000);
    this.destroyRef.onDestroy(() => window.clearInterval(id));
  }
}

Efekt jest taki sam: interwał zatrzyma się w chwili usunięcia komponentu. Różnica polega na tym, że start i sprzątanie stoją obok siebie, więc trudniej zapomnieć o drugim. Obie drogi są poprawne, a w zadaniach tego modułu ćwiczysz klasyczne ngOnDestroy.

Kolejność wywołań

Angular wywołuje hooki zawsze w tej samej kolejności. Oto pełna lista, od narodzin do odejścia:

  1. constructor - utworzenie instancji klasy (raz)
  2. ngOnChanges - przed ngOnInit, jeśli są inputy powiązane w szablonie, a potem po każdej ich zmianie (wielokrotnie)
  3. ngOnInit - inicjalizacja komponentu (raz)
  4. ngDoCheck - własne sprawdzanie zmian (wielokrotnie)
  5. ngAfterContentInit - po wstawieniu treści przekazanej przez <ng-content> (raz)
  6. ngAfterContentChecked - po sprawdzeniu tej treści (wielokrotnie)
  7. ngAfterViewInit - po wyświetleniu widoku i dzieci (raz)
  8. ngAfterViewChecked - po sprawdzeniu widoku (wielokrotnie)
  9. ngOnDestroy - tuż przed usunięciem komponentu (raz)

Nie musisz implementować wszystkich. W praktyce najczęściej sięgniesz po cztery: ngOnChanges, gdy reagujesz na nowe dane od rodzica, ngOnInit do startu, ngAfterViewInit, gdy potrzebujesz gotowego widoku, i ngOnDestroy do sprzątania. Konstruktor zawsze jest pierwszy, a ngOnDestroy zawsze ostatni. Hooki z końcówką Checked oraz ngDoCheck Angular wywołuje przy każdym sprawdzaniu zmian, czyli bardzo często, więc nie umieszczaj w nich ciężkich obliczeń. Pobieranie danych w ngOnInit rozwiniesz w dziewiątym module, przy komunikacji z API.

Pamiętaj: konstruktor przygotowuje ekwipunek, ngOnInit rozpoczyna walkę, a ngOnDestroy sprząta pole bitwy, zanim wojownik odejdzie.

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Który lifecycle hook jest idealny do pobierania danych z API przy inicjalizacji komponentu?

  2. 2. Który lifecycle hook służy do sprzątania zasobów (anulowanie subskrypcji, czyszczenie timerów) przed zniszczeniem komponentu?

  3. 3. Który lifecycle hook jest wywoływany przy każdej zmianie wartości @Input?

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj lifecycle hooks w kolejności ich wywoływania przez Angular (od pierwszego do ostatniego):

  • Edytor kodu

    Zaimplementuj interfejsy OnInit i OnDestroy oraz odpowiadające im metody cyklu życia (lifecycle hooks).

  • Układanie w poziomie

    Ułóż tokeny jednej linii deklaracji klasy implementującej hook OnInit:

Przydatne artykuły