Lifecycle Hooks (haki życia komponentu)

1. Czym są Lifecycle Hooks?

Lifecycle Hooks, czyli haki życia komponentu, to specjalne metody w Angularze, które oferują możliwość interwencji w różne momenty cyklu życia komponentów i dyrektyw. Dzięki nim, deweloperzy mogą wykonywać określone akcje w kluczowych momentach, takich jak inicjalizacja komponentu, detekcja zmian, czy też jego zniszczenie.

Analogia z życia:

Komponent = Człowiek 🧑

  • Narodziny (constructor) – pierwsze chwile życia
  • Pierwsze kroki (ngOnInit) – gotowość do działania
  • Dorastanie (ngOnChanges) – reagowanie na zmiany
  • Śmierć (ngOnDestroy) – pożegnanie i porządki

W aplikacji filmowej:

Film Component zostaje utworzony → ładuje dane → reaguje na zmiany → zostaje usunięty

2. Podstawowe Lifecycle Hooks

W Angularze istnieje kilka głównych haków życia komponentu, które pomagają zarządzać różnymi aspektami działania komponentu:

  1. constructor – tworzenie komponentu (nie jest lifecycle hook, ale ważny)
  2. ngOnChanges – wykrywanie zmian w danych wejściowych (@Input)
  3. ngOnInit – inicjalizacja komponentu (uruchamia się raz)
  4. ngDoCheck – ręczne sprawdzanie zmian
  5. ngAfterContentInit – po wstawieniu treści (ng-content)
  6. ngAfterContentChecked – po sprawdzeniu treści
  7. ngAfterViewInit – po inicjalizacji widoku
  8. ngAfterViewChecked – po sprawdzeniu widoku
  9. ngOnDestroy – przed usunięciem komponentu

3. Przykład z profilem użytkownika (Twój kod)

Definicja komponentu

// user-profile.component.ts
import { Component, Input, OnInit, OnDestroy, SimpleChanges, OnChanges } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `<p>Profil użytkownika: {{ userData.name }}</p>`,
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit, OnChanges, OnDestroy {
  @Input() userData: { name: string, age: number };

  constructor() {
    console.log('UserProfileComponent constructed');
  }

  ngOnChanges(changes: SimpleChanges): void {
    console.log('NgOnChanges triggered:', changes);
  }

  ngOnInit(): void {
    console.log('Component initialized with data:', this.userData);
  }

  ngOnDestroy(): void {
    console.log('Component is being destroyed');
  }
}

Użycie komponentu

<!-- Some parent component template -->
<app-user-profile [userData]="currentUser"></app-user-profile>

Wyjaśnienie kodu

  • Konstruktor (constructor): Używany do inicjalizacji podstawowych wartości, serwisów lub innych zależności. Nie jest to lifecycle hook, ale jest ważnym elementem inicjalizacji klasy w Angularze.
  • ngOnChanges: Reaguje na zmiany w @Input() properties. W przykładzie rejestruje zmiany w danych wejściowych userData.
  • ngOnInit: Idealne miejsce do inicjalizacji komponentu, np. pobrania danych z serwera. W przykładzie loguje dane użytkownika po ich inicjalizacji.
  • ngOnDestroy: Umożliwia wykonanie sprzątania, takiego jak anulowanie subskrypcji obserwabli. W przykładzie loguje informacje o zniszczeniu komponentu.

4. Praktyczne przykłady z aplikacją filmową

Przykład 1: Komponent karty filmu z ngOnInit i ngOnDestroy

// film-card.component.ts
import { Component, Input, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-film-card',
  standalone: true,
  template: `
    <div class="film-card">
      <h3>{{ film.title }}</h3>
      <p>Ocena: {{ film.rating }}/10</p>
      <p>Wyświetleń: {{ viewCount }}</p>
    </div>
  `,
  styles: [`
    .film-card {
      border: 1px solid #ddd;
      padding: 15px;
      margin: 10px;
      border-radius: 8px;
    }
  `]
})
export class FilmCardComponent implements OnInit, OnDestroy {
  @Input() film: any;
  viewCount = 0;
  private timer: any;

  ngOnInit() {
    console.log('🎬 Film Card created for:', this.film.title);
    
    // Symulacja licznika wyświetleń
    this.timer = setInterval(() => {
      this.viewCount++;
    }, 1000);
  }

  ngOnDestroy() {
    console.log('💀 Film Card destroyed for:', this.film.title);
    
    // WAŻNE: Zatrzymanie timera przed zniszczeniem
    if (this.timer) {
      clearInterval(this.timer);
    }
  }
}

Przykład 2: Komponent z ngOnChanges – reakcja na zmiany

// film-details.component.ts
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-film-details',
  standalone: true,
  template: `
    <div class="film-details">
      <h2>{{ film.title }}</h2>
      <p>{{ changeMessage }}</p>
      <p>Ostatnia zmiana: {{ lastChangeTime }}</p>
    </div>
  `,
  styles: [`
    .film-details {
      background: #f8f9fa;
      padding: 20px;
      border-radius: 8px;
    }
  `]
})
export class FilmDetailsComponent implements OnChanges {
  @Input() film: any;
  changeMessage = '';
  lastChangeTime = '';

  ngOnChanges(changes: SimpleChanges) {
    console.log('🔄 Changes detected:', changes);
    
    if (changes['film']) {
      const change = changes['film'];
      
      if (change.firstChange) {
        this.changeMessage = 'Film został załadowany po raz pierwszy';
      } else {
        this.changeMessage = `Film zmieniony z "${change.previousValue?.title}" na "${change.currentValue?.title}"`;
      }
      
      this.lastChangeTime = new Date().toLocaleTimeString();
    }
  }
}

Przykład 3: Komponent z ngAfterViewInit – dostęp do elementów DOM

// film-player.component.ts
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';

@Component({
  selector: 'app-film-player',
  standalone: true,
  template: `
    <div class="player">
      <video #videoPlayer width="400" height="300" controls>
        <source src="assets/videos/trailer.mp4" type="video/mp4">
      </video>
      <p>Status: {{ playerStatus }}</p>
    </div>
  `,
  styles: [`
    .player {
      text-align: center;
      padding: 20px;
    }
  `]
})
export class FilmPlayerComponent implements AfterViewInit {
  @ViewChild('videoPlayer') videoElement!: ElementRef;
  playerStatus = 'Inicjalizacja...';

  ngAfterViewInit() {
    console.log('🎥 Video player view initialized');
    
    // Teraz mamy dostęp do elementu video
    const video = this.videoElement.nativeElement;
    
    // Dodawanie event listenerów
    video.addEventListener('loadeddata', () => {
      this.playerStatus = 'Gotowy do odtwarzania';
    });
    
    video.addEventListener('play', () => {
      this.playerStatus = 'Odtwarzanie...';
    });
    
    video.addEventListener('pause', () => {
      this.playerStatus = 'Wstrzymano';
    });
  }
}

Przykład 4: Kompletny przykład z wieloma hooks

// film-manager.component.ts
import { Component, Input, OnInit, OnChanges, OnDestroy, AfterViewInit, SimpleChanges } from '@angular/core';
import { Subscription, interval } from 'rxjs';

@Component({
  selector: 'app-film-manager',
  standalone: true,
  template: `
    <div class="film-manager">
      <h3>{{ film.title }}</h3>
      <p>{{ statusMessage }}</p>
      <p>Uptime: {{ uptime }} sekund</p>
    </div>
  `,
  styles: [`
    .film-manager {
      border: 2px solid #007bff;
      padding: 15px;
      margin: 10px;
      border-radius: 8px;
    }
  `]
})
export class FilmManagerComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit {
  @Input() film: any;
  statusMessage = '';
  uptime = 0;
  private subscription?: Subscription;

  constructor() {
    console.log('🏗️ Constructor: Komponent tworzony');
    this.statusMessage = 'Konstruktor wykonany';
  }

  ngOnChanges(changes: SimpleChanges) {
    console.log('🔄 ngOnChanges: Wykryto zmiany');
    this.statusMessage = 'Dane zostały zmienione';
  }

  ngOnInit() {
    console.log('🚀 ngOnInit: Komponent inicjalizowany');
    this.statusMessage = 'Komponent zainicjalizowany';
    
    // Rozpocznij licznik czasu
    this.subscription = interval(1000).subscribe(() => {
      this.uptime++;
    });
  }

  ngAfterViewInit() {
    console.log('👀 ngAfterViewInit: Widok zainicjalizowany');
    this.statusMessage = 'Widok gotowy';
  }

  ngOnDestroy() {
    console.log('💀 ngOnDestroy: Komponent niszczony');
    
    // WAŻNE: Wyczyść subskrypcje
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }
}

5. Kolejność wykonywania hooks

Podczas tworzenia komponentu:

1. constructor()           // Tworzenie instancji
2. ngOnChanges()          // Jeśli są @Input properties
3. ngOnInit()             // Inicjalizacja (tylko raz)
4. ngDoCheck()            // Sprawdzanie zmian
5. ngAfterContentInit()   // Treść załadowana (tylko raz)
6. ngAfterContentChecked() // Treść sprawdzona
7. ngAfterViewInit()      // Widok załadowany (tylko raz)
8. ngAfterViewChecked()   // Widok sprawdzony

Podczas aktualizacji:

1. ngOnChanges()          // Gdy @Input się zmieni
2. ngDoCheck()            // Sprawdzanie zmian
3. ngAfterContentChecked() // Treść sprawdzona
4. ngAfterViewChecked()   // Widok sprawdzony

Podczas niszczenia:

1. ngOnDestroy()          // Sprzątanie przed zniszczeniem

6. Najczęstsze zastosowania

ngOnInit – Inicjalizacja

ngOnInit() {
  // ✅ Pobieranie danych z API
  this.loadFilmData();
  
  // ✅ Konfiguracja subskrypcji
  this.subscription = this.service.getData().subscribe();
  
  // ✅ Inicjalizacja zmiennych
  this.setupDefaults();
}

ngOnDestroy – Sprzątanie

ngOnDestroy() {
  // ✅ Anulowanie subskrypcji
  if (this.subscription) {
    this.subscription.unsubscribe();
  }
  
  // ✅ Zatrzymanie timerów
  if (this.timer) {
    clearInterval(this.timer);
  }
  
  // ✅ Usuwanie event listenerów
  window.removeEventListener('resize', this.onResize);
}

ngOnChanges – Reakcja na zmiany

ngOnChanges(changes: SimpleChanges) {
  if (changes['filmId']) {
    // ✅ Reaguj na zmianę ID filmu
    this.loadNewFilm(changes['filmId'].currentValue);
  }
}

7. Najlepsze praktyki

✅ Co robić:

  1. Zawsze implementuj interfejsyimplements OnInit, OnDestroy
  2. Czyść subskrypcje w ngOnDestroy – unikaj wycieków pamięci
  3. Używaj ngOnInit do inicjalizacji – nie constructor
  4. Sprawdzaj zmiany w ngOnChanges – użyj if (changes['propertyName'])

❌ Czego unikać:

  1. Ciężkie operacje w constructor – użyj ngOnInit
  2. Zapominanie o unsubscribe – wyciek pamięci!
  3. Manipulacja DOM w ngOnInit – użyj ngAfterViewInit
  4. Ignorowanie SimpleChanges – sprawdzaj co się zmieniło

Przykład dobrej praktyki:

export class GoodPracticeComponent implements OnInit, OnDestroy {
  private subscriptions = new Subscription();

  ngOnInit() {
    // Grupowanie subskrypcji
    this.subscriptions.add(
      this.service1.getData().subscribe()
    );
    this.subscriptions.add(
      this.service2.getData().subscribe()
    );
  }

  ngOnDestroy() {
    // Jedna linijka czyści wszystkie subskrypcje
    this.subscriptions.unsubscribe();
  }
}

8. Ćwiczenia do wykonania

  1. Timer komponentu – stwórz komponent z licznikiem używając ngOnInit/ngOnDestroy
  2. Dynamiczne szczegóły – komponent reagujący na zmiany ID filmu (ngOnChanges)
  3. Chat komponent – subskrypcje na wiadomości z proper cleanup