Siema! Dzisiaj na warsztat wjeżdża nowiutki temat. Lifecycle hooks, czyli zarządzanie życiem komponentu. Żeby nie było za dużo i za długo zaczniemy od inicjalizacji. Angular udostępnia mechanizmy pozwalające na wpięcie się w odpowiedni moment tworzenia, życia (upadet’u) lub destrukcji każdej klasy udekorowanej @Component().
Jak to działa?
Zaimplementuj tzw. hook’i, czyli interfejsy udostępniające metody wywoływane właśnie w odpowiednich momentach. Metody te wywołuje angular, więc Ty zatroszcz się tylko o ich odpowiednią implementację. Interfejsów jest aż osiem. Używasz tylko tych, których aktualnie potrzebujesz. Nie ma potrzeby wykorzystywania wszystkiego. Co za dużo to niezdrowo przecież 🙂
W tym wpisie omówimy hook’i odpowiedzialne za inicjalizację komponentu. Istnieją takie TRZY. Dodatkowo poznasz jeszcze ostatni, odpowiedzialny za niszczenie klasy. Zarządzanie cyklem życia. Pozostałymi zajmiemy się po zrozumieniu sposobu działania mechanizmu ChangeDetection.
OnInit (ngOnInit())
Kiedy się woła:
Tylko raz, zawsze w czasie inicjalizacji. Czyli po konstruktorze, ale przed pełnym powstaniem instancji klasy komponentowej. Pamiętaj, że metoda wywoła się po pierwszym wyświetleniu danych, czyli ich zmiana spowoduje update widoku.
Kiedy stosować:
- Prawidłowa inicjalizacja komponentu.
- Subskrypcje na zewnętrzne subjecty rxjs
- Pobieranie danych z API (pobierz raz i wyświetl)
- Wykorzystanie wartości zbindowanych zmiennych (@Input()).
AfterViewInit (ngAfterViewInit())
Kiedy się woła:
Tylko raz, po ngOnInit(). Między nimi są jeszcze 3 hook’i do odpalenia, ale dzisiaj zostawiamy je na inną okazję. Zapamiętaj tylko, że najpierw wywołana jest ngOnInit, potem ngAfterViewInit.
Kiedy stosować:
- Chcesz wykonać funkcjonalność po tym, jak komponent i jego dzieci się zainicjalizowały
- Chcesz dobrać się do dziecka poprzez @ViewChild.
OnDestroy (ngOnDestroy())
Kiedy się woła:
Tylko raz w trakcie niszczenia instancji klasy komponentu.
Kiedy stosować:
- Sprzątanie po komponencie
- Anuluj wszystkie subskrypcje, odłącz obsługę zdarzeń
- Anuluj timer’y
- Wyrejestruj wszystkie zależności do zewnętrznych serwisów
Jest jeszcze jeden hook, wołający się podczas inicjalizacji – AfterContentInit. Jednak do zrozumienia pełni jego możliwości potrzebujesz nieco bardziej zaawansowanej wiedzy. Dzisiaj wystarczy Ci jedynie informacja, że woła się on po ngOnInit(), a przed ngAfterViewInit(). Na dziś zarezerwuj sobie na niego szufladkę w głowie. Omówię go szczegółowo niebawem.
Kolejność jest taka:
constructor() -> nieważny_hook() -> ngOnInit() -> nieważny_hook2() -> ngAfterContentInit() -> nieważny_hook3() -> ngAfterViewInit() -> nieważny_hook4() -> nieważny_hook5() -> nieważny_hook6() -> ngOnDestroy()
Jako nieważny_hook() traktuj metody z lifecycle hooks, które przedstawię niebawem. Nie są one istotne z punktu widzenia tego wpisu.
Czas na kod!
Stwórzmy dwa komponenty ParentComponent i osadzony w nim ChildComponent:
@Component({ selector: "parent", template: ` <child></child> ` }) export class ParentComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { ngOnInit(): void { console.log("Parent ngOnInit"); } ngAfterContentInit(): void { console.log("Parent ngAfterContentInit"); } ngAfterViewInit(): void { console.log("Parent ngAfterViewInit"); } ngOnDestroy(): void { console.log("Parent ngOnDestroy"); } }
@Component({ selector: "child", template: ` Child component ` }) export class ChildComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { ngOnInit(): void { console.log("Child ngOnInit"); } ngAfterContentInit(): void { console.log("Child ngAfterContentInit"); } ngAfterViewInit(): void { console.log("Child ngAfterViewInit"); } ngOnDestroy(): void { console.log("Child ngOnDestroy"); } }
Jak widzisz, każdy z tych komponentów implementuje omówione hook’i. Implementacja jest mega prosta, każda metoda melduje swoje wywołanie. Sprawdźmy teraz jak to wygląda:
ChildComponent jest częścią ParentComponent, dlatego inicjalizuje się podczas jego startu. Najpierw wstaje ParentComponent (ngOnInit()), potem tworzy swoją dodatkową treść (ngAfterContentInit() – w tym przypadku tej treści nie ma, ale hook wywołuje się). Kolejny krok to pełna inicjalizacja parenta, podczas której tworzy i inicjalizuje w pełni ChildComponent. Kiedy już to zrobi angular woła ostatni z hook’ów ParentComponent – ngAfterViewInit().
Teoria teorią, ale po co to wszystko?
Przykład jest mega prosty. Obrazuje jedynie kolejność wstawania komponentów. Jednak jakie jest praktyczne zastosowanie hook’ów? Sprawdźmy teraz, w którym dokładnie momencie parent wie, że jego child już powstał.
Kod klasy ParentComponent wygląda teraz tak:
export class ParentComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { @ViewChild(ChildComponent) private readonly child: ChildComponent; ngOnInit(): void { console.log("Parent ngOnInit", this.child); } ngAfterContentInit(): void { console.log("Parent ngAfterContentInit", this.child); } ngAfterViewInit(): void { console.log("Parent ngAfterViewInit", this.child); } ngOnDestroy(): void { console.log("Parent ngOnDestroy"); } }
Widzisz tu praktyczne zastosowanie dekoratora @ViewChild(), omówionego w tym wpisie. Używam console.log do wypisania obiektu dziecka w każdym hook’u.
I wszystko jasne! Na etapie inicjalizacji komponentu możesz odwoływać się do jego dzieci. Ale dopiero w momencie, gdy one powstaną! Czyli w ngAfterViewInit(). Dopiero w trakcie wywołania ngAfterViewInit() referencja na dziecko będzie zdefiniowana i możesz z niej korzystać. Na każdym wcześniejszym etapie inicjalizacji komponentu pod zmienną this.child otrzymasz undefined.
Co teraz?
Do ChildComponent dołóżmy prostą zmienną, zbindowaną z parenta. Dodatkowo wyświetlmy jej zawartość na etapie tworzenia i inicjalizacji komponentu:
No dobra, ale co z pozostałymi hook’ami? Czy są bezużyteczne? Nic z tych rzeczy. Sprawdźmy zachowanie OnInit. Bardziej zaawansowane wykorzystanie masz omówione powyżej. Tutaj prosty przykład:
export class ChildComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { @Input() private var: string; public constructor() { console.log("Child constructor", this.var); } ngOnInit(): void { console.log("Child ngOnInit", this.var); } ngAfterContentInit(): void { console.log("Child ngAfterContentInit"); } ngAfterViewInit(): void { console.log("Child ngAfterViewInit"); } ngOnDestroy(): void { console.log("Child ngOnDestroy"); } }
Przekażmy zmienną z ParentComponent:
<child var="zmienna"></child>
Oto co otrzymaliśmy w logach:
Na etapie tworzenia komponentu nie możesz odwoływać się do zmiennych przekazywanych poprzez @Input() – data bounded properties. Po prostu, angular jeszcze nic nie wie o tym, co za chwilę trafi do komponentu. Dopiero w momencie inicjalizacji masz dostęp do tej wiedzy. Tutaj moźesz wykonać dodatkową logikę, na bazie przekazanych danych.
Dodatkowo dołożyłem logi w konstruktorach klas. Pamiętaj, że:
Angular tworzy obiekty komponentów na długo przed ich inicjalizacją!
Najpierw woła się konstruktor parenta, to zrozumiałe. W pierwszej kolejności musi powstać instancja klasy głównej. Jednak to co dzieje się potem już takie oczywiste nie jest. Tworzy się instancja child’a. Dopiero potem angular rozpoczyna inicjalizację tych klas.
OnDestroy
Ok, a co z ngOnDestroy()? Jak już wspomniałem odpala się podczas niszczenia komponentu. Do prezentacji potrzebujemy dołożyć nieco logiki do ParentComponent’u. Stwórzmy flagę, na bazie której będziemy tworzyć lub niszczyć ChildComponent. Flaga zmienia wartość po każdym kliknięciu w przycisk:
export class ParentComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { public flag = true; … changeFlagValue(): void { console.log('Clicked, actual flag: ', this.flag); this.flag = !this.flag; } … }
Jeszcze tylko modyfikacja szablonu:
<button (click)="changeFlagValue()">Click Me</button> <child *ngIf=flag var="zmienna"></child>
Widzisz coś nowego? *ngIf=flag to dyrektywa strukturalna angulara. Pozwala na zamieszczanie prostej logiki wewnątrz szablonu. Działa tak samo jak instrukcja warunkowa if() w dowolnym języku programowania. W tym przypadku, w zależności od wartości zmiennej flag, instrukcja stworzy lub zniszczy ChildComponent:
Po kliknięciu przycisku ChildComponent znika oraz wywołuje się jego ngOnDestroy:
Po ponownym kliknięciu angular tworzy i na nowo inicjalizuje ChildComponent (ale już bez wołania hook’ów parenta):
To tyle na dziś!