Lifecycle hook adalah method yang dipanggil Angular sendiri pada saat-saat tertentu dalam hidup sebuah komponen: saat input dari induknya datang atau berubah, sesudah komponennya siap, sesudah tampilannya ada di DOM, dan tepat sebelum komponennya dihapus. Bayangin satu shift juru masak di dapur Bang Jali. Dia datang dan absen (constructor), lalu menerima kertas pesanan (input masuk), mulai memasak (ngOnInit), martabaknya sudah di piring dan bisa dipegang (ngAfterViewInit), pesanannya diganti di tengah jalan (ngOnChanges lagi), lalu pulang dan mematikan kompor (ngOnDestroy). Lupa mematikan kompor, apinya tetap menyala walau orangnya sudah pulang.
🧪 Tebakan cepat: Pesanan punya
@Input() menu = '', dan App menulis<app-pesanan [menu]="menu" />denganmenu = 'Martabak Telur Spesial'. Di constructor Pesanan adaconsole.log('constructor: menu =', JSON.stringify(this.menu)). Apa yang tercetak? Jawabannya ada di bagian "constructor atau ngOnInit".
Urutan hook
import { AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'
@Component({
selector: 'app-pesanan',
standalone: true,
template: `
<p>{{ judul }}</p>
<input #catatan placeholder="Catatan" />
`,
})
export class Pesanan implements OnChanges, OnInit, AfterViewInit, OnDestroy {
@Input() menu = ''
@ViewChild('catatan') catatan!: ElementRef<HTMLInputElement>
judul = ''
private timer = 0
constructor() {
console.log('constructor: menu =', JSON.stringify(this.menu))
}
ngOnChanges(changes: SimpleChanges) {
const c = changes['menu']
console.log('ngOnChanges:', JSON.stringify(c.previousValue), '->', JSON.stringify(c.currentValue))
}
ngOnInit() {
console.log('ngOnInit: menu =', JSON.stringify(this.menu))
this.judul = 'Pesanan: ' + this.menu
this.timer = setInterval(() => console.log('cek status', this.menu), 2000)
}
ngAfterViewInit() {
console.log('ngAfterViewInit: catatan =', this.catatan.nativeElement.tagName)
}
ngOnDestroy() {
clearInterval(this.timer)
console.log('ngOnDestroy')
}
}
App menampilkannya dengan @if (buka) { <app-pesanan [menu]="menu" /> }, plus tombol Ganti menu dan Tutup. Console-nya, persis seperti yang dicetak Angular 18:
constructor: menu = ""
ngOnChanges: undefined -> "Martabak Telur Spesial"
ngOnInit: menu = "Martabak Telur Spesial"
ngAfterViewInit: catatan = INPUT
(klik Ganti menu)
ngOnChanges: "Martabak Telur Spesial" -> "Martabak Manis Cokelat Keju"
(klik Tutup)
ngOnDestroy
| Hook | Kapan dipanggil | Biasanya dipakai untuk |
|---|---|---|
constructor |
saat class-nya dibuat, sebelum input diisi | inject() dan nilai awal properti |
ngOnChanges |
sebelum ngOnInit, lalu tiap kali input dari induk berganti nilai |
menghitung ulang dari input |
ngOnInit |
sekali, sesudah ngOnChanges pertama |
kerja awal yang butuh input: ambil data, mulai langganan |
ngAfterViewInit |
sekali, sesudah template komponen ini ada di DOM | @ViewChild, fokus ke elemen |
ngOnDestroy |
sekali, tepat sebelum komponennya dihapus | mematikan timer dan langganan |
implements OnInit gak wajib buat Angular, yang memanggil method-nya berdasarkan nama. Gunanya untuk TypeScript: di proyek CLI, salah ketik seperti ngOnInt() langsung ditolak karena class-nya gak lagi memenuhi OnInit.
constructor atau ngOnInit
Angular membuat class-nya dulu, baru mengisi input dari induknya. Saat constructor jalan, menu masih berisi nilai awalnya sendiri, ''. Jadi yang tercetak constructor: menu = "", bukan nama martabaknya. Itu jawaban tebakannya. Input tanpa nilai awal (@Input() menu!: string) malah berisi undefined.
ngOnInit dipanggil sesudah input pertama diisi, jadi this.menu di sana sudah 'Martabak Telur Spesial'. Aturannya: constructor untuk inject() dan nilai awal, ngOnInit untuk kerja yang butuh input. Latihan memakai pola yang sama: message = 'Loading...' diganti jadi 'Komponen sudah diinisialisasi!' di dalam ngOnInit(), dan <p>{{ message }}</p> menampilkannya.
Tapi ngOnInit cuma jalan sekali. Sesudah Ganti menu, judul tetap Pesanan: Martabak Telur Spesial, walau menu sudah Martabak Manis Cokelat Keju. Yang perlu mengikuti input setiap kali, taruh di ngOnChanges, atau baca {{ menu }} langsung di template.
ngOnChanges dan SimpleChanges
ngOnChanges(changes: SimpleChanges) {
if (changes['menu']) {
this.judul = 'Pesanan: ' + changes['menu'].currentValue
}
}
changesberisi satu entri per input yang berubah:previousValue,currentValue, danfirstChange(trueuntuk panggilan pertama sebelumngOnInit).- Cuma dipanggil untuk input yang diikat induknya. Komponen yang dipakai sebagai
<app-pesanan />tanpa[menu]gak pernah memanggilngOnChanges, dan perubahan properti biasa di dalam komponennya sendiri juga gak memicunya. - Dibandingkan per nilai. Mengirim teks yang sama lagi gak memanggil apa pun, dan mengubah isi objek (
this.data.jumlah = 2) juga gak, karena objeknya masih objek yang sama. Kirim objek baru:this.data = { ...this.data, jumlah: 2 }.
ngOnDestroy: matikan yang kamu nyalakan
setInterval di ngOnInit terus jalan sampai dimatikan. Tanpa clearInterval di ngOnDestroy, ini yang terjadi sesudah Tutup:
| Waktu | Dengan clearInterval |
Tanpa clearInterval |
|---|---|---|
| Pesanan terbuka | cek status tiap 2 detik |
cek status tiap 2 detik |
| Sesudah Tutup | sepi | cek status tetap tercetak tiap 2 detik |
| Buka lagi | satu timer | dua timer, dua baris tiap 2 detik |
Ini memory leak yang sama dengan langganan timer di pelajaran RxJS: yang kamu nyalakan sendiri (interval, langganan, event listener di window), kamu matikan di ngOnDestroy. Latihan kedua memakai dua hook: ngOnInit mengisi status = 'Active' sehingga <p> berbunyi Status: Active, dan ngOnDestroy mencetak Component destroyed.
ngAfterViewInit dan @ViewChild
@ViewChild('catatan') baru terisi sesudah template komponennya dibuat. Di constructor dan ngOnInit isinya undefined; di ngAfterViewInit sudah ElementRef berisi <input>, jadi this.catatan.nativeElement.focus() aman ditulis di sana.
Jangan mengubah nilai yang sedang ditampilkan template di dalam ngAfterViewInit. Misalnya App menampilkan {{ judul }} dengan judul = 'awal', lalu ngAfterViewInit-nya menulis this.judul = 'Pesanan untuk ' + this.pesanan.menu. Tampilannya sudah selesai diperiksa. Di komponen Eager (bawaan sampai Angular 21), Angular dalam mode development memprotes: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'awal'. Current value: 'Pesanan untuk Martabak Telur Spesial'. Expression location: App component. Di Angular 22, yang komponennya OnPush secara bawaan, gak ada error sama sekali, dan judulnya tetap awal (dicoba di editor latihan). Dua-duanya tanda nilainya ditulis di tempat yang salah: hitung di ngOnInit, atau simpan di signal.
⚠️ Jebakan yang sering ketemu
- Membaca input di constructor. Isinya masih nilai awal (
'') atauundefined. Pindahkan kengOnInit. - Menghitung dari input cuma di
ngOnInit. Saat induknya mengganti input,ngOnInitgak jalan lagi. PakaingOnChanges. - Mengubah isi objek lalu menunggu
ngOnChanges. Objek yang sama gak dianggap berubah; kirim objek baru. - Gak membersihkan di
ngOnDestroy. Timer dan langganan tetap jalan sesudah komponennya hilang. - Detail latihan. Penilai mencari
this.<properti> =di dalam tubuhngOnInit(), kataOnInit(danOnDestroydi latihan kedua), dan<p>yang isinya diambil dari{{ }}, bukan kalimat yang diketik di template.
TL;DR: Urutan hook komponen: constructor, ngOnChanges, ngOnInit, ngAfterViewInit, dan di akhir ngOnDestroy. Input belum terisi di constructor (yang tercetak nilai awalnya sendiri), jadi kerja yang butuh input ditaruh di ngOnInit, yang cuma jalan sekali. ngOnChanges(changes: SimpleChanges) jalan tiap kali induk mengirim nilai input yang baru, dengan previousValue dan currentValue; objek yang isinya diubah gak dihitung. @ViewChild baru terisi di ngAfterViewInit. Interval dan langganan yang dinyalakan sendiri dimatikan di ngOnDestroy, atau mereka terus jalan sesudah komponennya hilang. implements OnInit membuat TypeScript memeriksa nama method-nya.