Kurs Angular · Moduł 2: Komponenty
Lifecycle hooks - rytuały wojownika
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:
- constructor - utworzenie instancji klasy (raz)
- ngOnChanges - przed
ngOnInit, jeśli są inputy powiązane w szablonie, a potem po każdej ich zmianie (wielokrotnie) - ngOnInit - inicjalizacja komponentu (raz)
- ngDoCheck - własne sprawdzanie zmian (wielokrotnie)
- ngAfterContentInit - po wstawieniu treści przekazanej przez
<ng-content>(raz) - ngAfterContentChecked - po sprawdzeniu tej treści (wielokrotnie)
- ngAfterViewInit - po wyświetleniu widoku i dzieci (raz)
- ngAfterViewChecked - po sprawdzeniu widoku (wielokrotnie)
- 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. Który lifecycle hook jest idealny do pobierania danych z API przy inicjalizacji komponentu?
2. Który lifecycle hook służy do sprzątania zasobów (anulowanie subskrypcji, czyszczenie timerów) przed zniszczeniem komponentu?
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: