Mert Özen Her Satırda Daha İleri
Frontend Angular — #08

Angular Lifecycle Hook'ları: ngOnInit'ten ngOnDestroy'a Bir Component'in Hayatı

Mert Özen 31 Eki 2026 7 dk 0 görüntülenme
Angular Lifecycle Hook'ları: ngOnInit'ten ngOnDestroy'a Bir Component'in Hayatı

Constructor ile ngOnInit farkı, SimpleChanges ile ngOnChanges, ViewChild'ın hazır olduğu ngAfterViewInit ve interval temizleyen ngOnDestroy. Tüm hook'ların sırasını loglarla görüyoruz.

Sürüm notu: Bu yazıdaki kod ve komutlar Angular 12 içindir.

Bir Component'in Doğumdan Ölüme Hikâyesi

Daha önce gördüğümüz @ViewChild ile bir child component'e ya da bir input elemanına erişmeye çalıştıysan, bir noktada undefined ile karşılaşmış olabilirsin. Kod doğru, decorator doğru, ama değer gelmiyor. Sebep basit: o satırı yanlış zamanda çalıştırıyorsun.

Angular'da her component'in bir hayatı var. Oluşturulur, input'larını alır, template'i çizilir, veriler değiştikçe güncellenir ve bir gün ekrandan kaldırılır. Angular bu yolculuğun belli duraklarında senin yazdığın metodları çağırır. Bu metodlara lifecycle hook denir.

Yeni bir eve taşınmak gibi düşün. Anahtarı teslim aldığın an constructor'dır; ev senin ama içi boş. Eşyalar gelip yerleştiğinde ngOnInit çalışır. Evden çıkarken elektrik ve su aboneliklerini kapatmak da ngOnDestroy'un işidir. Kapatmazsan faturalar gelmeye devam eder.

Constructor mı, ngOnInit mi?

En sık karıştırılan ikiliden başlayalım. ng generate component ile ürettiğin her component'te ikisi de hazır geliyordu. Şu component'e bak:

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

@Component({
  selector: 'app-user-profile',
  template: `<p>{{ profileText }}</p>`
})
export class UserProfileComponent implements OnInit {
  @Input() userId = 0;
  profileText = '';

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

  ngOnInit(): void {
    console.log('ngOnInit, userId:', this.userId);
    this.profileText = `Kullanıcı #${this.userId} yükleniyor...`;
  }
}

Parent template'te bu component'i <app-user-profile [userId]="42"></app-user-profile> diye kullanıyoruz. Console'a bakınca constructor satırında 0, ngOnInit satırında 42 yazdığını görürsün.

Constructor, TypeScript class'ının kendi kurucusudur ve Angular onu component'i oluştururken çağırır. Bu anda input'lar henüz atanmamıştır; Angular önce nesneyi yaratır, değerleri sonra verir. ngOnInit ise input'lar ilk kez atandıktan sonra, bir kere çalışır.

Pratikteki kural şu: constructor'ı ihtiyaç duyduğun bağımlılıkları almak için kullan, başka iş yükleme. Bu bağımlılıkların nasıl geldiğini, yani dependency injection'ı ilerleyen yazılarda detaylandıracağız. Input'a bağlı her başlangıç işi, veri yükleme, hesaplama gibi şeyler ngOnInit'e gider.

Class'taki implements OnInit kısmı zorunlu değil; Angular metodu adından tanır. Ama interface'i yazarsan metod adında bir harf hatası yaptığında TypeScript seni uyarır. Her zaman yaz.

ngOnChanges: Input Değiştiğinde Haber Al

ngOnInit yalnızca bir kere çalışır. Peki input sonradan değişirse? Bir fiyat etiketi düşün; fiyat arttıysa yukarı, düştüyse aşağı ok göstersin:

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

@Component({
  selector: 'app-price-tag',
  template: `<p>{{ price }} TL <span *ngIf="trend">{{ trend }}</span></p>`
})
export class PriceTagComponent implements OnChanges {
  @Input() price = 0;
  trend = '';

  ngOnChanges(changes: SimpleChanges): void {
    const change = changes['price'];
    if (change && !change.firstChange) {
      this.trend = change.currentValue > change.previousValue ? '▲' : '▼';
    }
  }
}

ngOnChanges, parent'tan gelen bir input'un değeri her değiştiğinde çağrılır. İlk çağrı ngOnInit'ten bile öncedir. Parametre olarak gelen SimpleChanges nesnesi, değişen her input için bir kayıt tutar: previousValue eski değer, currentValue yeni değer, firstChange da bunun ilk atama olup olmadığı.

Biz firstChange kontrolüyle ilk atamayı atlıyoruz, çünkü ilk gelişte kıyaslanacak bir eski fiyat yok. Sonraki her değişimde eski ve yeni değeri karşılaştırıp oku seçiyoruz.

Bir tuzağa dikkat et. ngOnChanges referans değişimini yakalar. Input olarak bir dizi verdin ve parent o diziye push ile eleman eklediyse, dizi hâlâ aynı nesne olduğu için ngOnChanges çalışmaz. Yeni bir dizi atarsan çalışır. Bunun arka planını change detection konusunu işlerken detaylandıracağız.

ngDoCheck: Her Turda Uğrayan Bekçi

Angular, bir event, bir timer ya da başka bir tetikleyiciden sonra ekranı güncellemek için component ağacını baştan sona kontrol eder. ngDoCheck, bu kontrolün her turunda çalışır. Input değişmiş olsun ya da olmasın.

Az önceki push sorununu burada kendi kontrolünle yakalayabilirsin; örneğin dizinin uzunluğunu bir önceki turla karşılaştırırsın. Ama bu hook saniyede onlarca kez çalışabilir. Bir sitenin bekçisi gibi, her turda kapıları tek tek yoklar. İçine ağır bir iş koyarsan bütün uygulama yavaşlar. Varlığını bil, nadiren kullan.

Content ve View Hook'ları: ViewChild Ne Zaman Hazır?

Yazının başındaki undefined sorununa dönelim. Bir arama kutusu, ekran açılır açılmaz odaklansın istiyoruz:

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

@Component({
  selector: 'app-search-box',
  template: `<input #searchInput placeholder="Ürün ara...">`
})
export class SearchBoxComponent implements OnInit, AfterViewInit {
  @ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;

  ngOnInit(): void {
    console.log(this.searchInput); // undefined
  }

  ngAfterViewInit(): void {
    this.searchInput.nativeElement.focus();
  }
}

ngOnInit çalıştığında template henüz çizilmemiştir, o yüzden searchInput boştur. ngAfterViewInit ise component'in kendi template'i ve içindeki child component'ler tamamen oluşturulduktan sonra bir kere çalışır. ViewChild'a güvenle dokunabileceğin ilk yer burasıdır.

Property'nin sonundaki ünlem işareti Angular 12'nin strict modundan geliyor. TypeScript, her property'nin bir başlangıç değeri olmasını ister; ! ile "bunu Angular dolduracak, merak etme" demiş oluyoruz.

Eleman bir *ngIf içinde değilse @ViewChild('searchInput', { static: true }) yazarak ona ngOnInit'te de erişebilirsin. Bu durumda Angular sorguyu ilk çizimden önce çözer.

View hook'larının bir de content kardeşleri var. Daha önce gördüğümüz ng-content ile dışarıdan yansıtılan içerik, component'in kendi template'inden önce yerleşir. ngAfterContentInit tam o an çalışır ve @ContentChild ile yakaladığın elemanlar burada hazırdır:

@ContentChild('cardTitle') cardTitle?: ElementRef;

ngAfterContentInit(): void {
  console.log('Başlık geldi mi?', !!this.cardTitle);
}

Bu kod, <app-card><h3 #cardTitle>Sipariş Özeti</h3></app-card> şeklinde kullanılan bir kart component'inin içinde duruyor. Yansıtılan başlık yerleşince true yazar.

ngAfterContentChecked ve ngAfterViewChecked ise Init'lerin "her turda" versiyonları. ngDoCheck gibi sık çalışırlar, aynı uyarı geçerli.

Son bir not: ngAfterViewInit içinde template'e bağlı bir property'yi değiştirirsen, geliştirme modunda ExpressionChangedAfterItHasBeenCheckedError hatası alırsın. Angular ekranı çoktan kontrol etmiş, sen de arkasından değeri değiştirmişsin. Odaklama gibi DOM işleri burada sorun çıkarmaz, ekranda gösterilen veriyi değiştirmek çıkarır.

ngOnDestroy: Çıkarken Işıkları Kapat

Bir component *ngIf ile kaldırıldığında ya da başka bir sayfaya geçildiğinde yok edilir. Angular bundan hemen önce ngOnDestroy'u çağırır. Şu saat component'ine bak:

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

@Component({
  selector: 'app-clock',
  template: `<p>{{ now.toLocaleTimeString() }}</p>`
})
export class ClockComponent implements OnInit, OnDestroy {
  now = new Date();
  private timerId = 0;

  ngOnInit(): void {
    this.timerId = window.setInterval(() => {
      this.now = new Date();
      console.log('tik');
    }, 1000);
  }

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

ngOnInit'te saniyede bir çalışan bir interval kuruyoruz ve id'sini saklıyoruz. ngOnDestroy'da o id ile interval'i durduruyoruz.

ngOnDestroy'u yazmasaydık ne olurdu? Component ekrandan gider ama interval tarayıcıda yaşamaya devam eder. Console'a "tik" yazılmayı sürdürür. Component'i her gösterip gizlediğinde yeni bir interval eklenir ve hepsi aynı anda çalışır. Bu, klasik bir memory leak.

Aynı mantık window'a eklediğin event listener'lar ve observable subscription'ları için de geçerli. Subscription'ları temizlemenin düzgün yolunu RxJS'i anlatırken ilerleyen yazılarda detaylandıracağız. Kural değişmez: ngOnInit'te ne açtıysan, ngOnDestroy'da kapat.

Sıralamayı Kendi Gözünle Gör

Teoriyi bir kenara bırakıp bütün hook'ları tek bir component'te loglayalım:

import {
  AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit,
  Component, DoCheck, Input, OnChanges, OnDestroy, OnInit, SimpleChanges
} from '@angular/core';

@Component({
  selector: 'app-lifecycle-demo',
  template: `<p>Sayaç: {{ count }}</p>`
})
export class LifecycleDemoComponent implements OnChanges, OnInit, DoCheck,
  AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
  @Input() count = 0;

  constructor() { this.log('constructor'); }
  ngOnChanges(changes: SimpleChanges): void { this.log('ngOnChanges'); }
  ngOnInit(): void { this.log('ngOnInit'); }
  ngDoCheck(): void { this.log('ngDoCheck'); }
  ngAfterContentInit(): void { this.log('ngAfterContentInit'); }
  ngAfterContentChecked(): void { this.log('ngAfterContentChecked'); }
  ngAfterViewInit(): void { this.log('ngAfterViewInit'); }
  ngAfterViewChecked(): void { this.log('ngAfterViewChecked'); }
  ngOnDestroy(): void { this.log('ngOnDestroy'); }

  private log(hook: string): void {
    console.log(`[demo] ${hook}`);
  }
}

AppComponent'e count = 0 ve visible = true property'lerini ekle, template'i de şöyle yaz:

<button (click)="count = count + 1">Artır</button>
<button (click)="visible = !visible">Göster/Gizle</button>
<app-lifecycle-demo *ngIf="visible" [count]="count"></app-lifecycle-demo>

Sayfa açıldığında console'da şu sırayı görürsün:

[demo] constructor
[demo] ngOnChanges
[demo] ngOnInit
[demo] ngDoCheck
[demo] ngAfterContentInit
[demo] ngAfterContentChecked
[demo] ngAfterViewInit
[demo] ngAfterViewChecked

Artır butonuna bastığında Init'ler bir daha çalışmaz. Sadece değişimi bildiren ve her turda çalışanlar gelir:

[demo] ngOnChanges
[demo] ngDoCheck
[demo] ngAfterContentChecked
[demo] ngAfterViewChecked

Gizle'ye basınca da tek satır düşer: [demo] ngOnDestroy. Tekrar gösterdiğinde baştaki sekiz satırın hepsi yeniden gelir, çünkü bu yepyeni bir component nesnesidir.

Uygulama açılırken Checked satırlarının birkaç kez daha yazıldığını görebilirsin. Angular başlangıçta birden fazla kontrol turu yapabilir; sıra aynı kaldığı sürece endişelenecek bir şey yok.

Küçük Bir Deneme

ClockComponent'i projene ekle ve AppComponent'te bir butonla *ngIf üzerinden göster/gizle yap. Önce ngOnDestroy içindeki clearInterval satırını yoruma al. Saati beş altı kez açıp kapat ve console'daki "tik" sayısının nasıl katlandığını izle. Sonra satırı geri aç ve farkı gör.

İkinci adım olarak PriceTagComponent'i parent'a bağla. Parent'ta fiyatı rastgele artırıp azaltan bir buton koy ve okun doğru yöne döndüğünü kontrol et. firstChange kontrolünü kaldırınca ilk açılışta ne gördüğüne de bak.

Saati gösterirken toLocaleTimeString() çağırdık; fiyatın yanına da elle "TL" yazdık. Tarihi istediğin formatta göstermek, sayıyı para birimiyle biçimlendirmek için class'a yardımcı metodlar yazmak yerine bu dönüşümü template'te tek satırda yapmanın bir yolu var. Bir sonraki yazıda Angular'ın pipe'larına bakacağız.