Menguasai Lifecycle Custom Elements Vanilla: Membangun Web Components yang Robust dan Prediktif
1. Pendahuluan
Di dunia pengembangan web modern, kita sering berhadapan dengan kompleksitas UI yang terus meningkat. Komponen-komponen menjadi blok bangunan utama, dan banyak dari kita akrab dengan framework seperti React, Vue, atau Angular untuk mengelola komponen-komponen ini. Namun, ada satu standar web yang semakin matang dan kuat: Web Components.
Web Components memungkinkan kita membuat elemen HTML kustom yang bisa digunakan ulang, terenkapsulasi, dan framework-agnostic. Ini berarti komponen yang Anda buat dengan Web Components bisa digunakan di proyek React, Vue, Angular, bahkan di proyek HTML/JavaScript biasa, tanpa dependensi framework. Keren, kan?
Artikel-artikel lain di blog ini sudah membahas pengenalan Web Components, penggunaan Shadow DOM, hingga bagaimana mereka bisa menjadi fondasi Micro-Frontends. Tapi, untuk benar-benar memanfaatkan kekuatan Web Components, terutama saat Anda membangunnya dengan “vanilla” JavaScript tanpa bantuan library seperti Lit, ada satu hal krusial yang harus Anda kuasai: Lifecycle Custom Elements.
Memahami siklus hidup sebuah Custom Element itu seperti memahami kapan sebuah aplikasi hidup, bernapas, berinteraksi, dan akhirnya mati. Tanpa pemahaman ini, komponen Anda bisa jadi tidak efisien, sulit didebug, atau bahkan rentan terhadap memory leak.
Mari kita selami lebih dalam bagaimana Custom Elements “hidup” dan “mati” di DOM, dan bagaimana kita bisa mengontrolnya untuk membangun komponen yang robust dan prediktif.
2. Apa Itu Custom Elements dan Kenapa Lifecycle Itu Penting?
📌 Custom Elements adalah salah satu dari empat spesifikasi inti Web Components (bersama Shadow DOM, HTML Templates, dan ES Modules). Ini adalah API JavaScript yang memungkinkan Anda mendefinisikan elemen HTML baru dengan tag kustom Anda sendiri (misalnya, <my-button>, <user-profile>).
Setiap Custom Element yang Anda buat adalah sebuah class JavaScript yang mewarisi dari HTMLElement (atau elemen HTML lainnya seperti HTMLButtonElement). Class ini kemudian didaftarkan ke browser menggunakan customElements.define().
// my-button.js
class MyButton extends HTMLElement {
// ... kode komponen ...
}
customElements.define('my-button', MyButton);
Ketika browser menemukan tag <my-button> di HTML, ia akan membuat instance dari class MyButton dan mengelola siklus hidupnya. Nah, di sinilah lifecycle callbacks berperan. Mereka adalah metode khusus di dalam class Custom Element Anda yang secara otomatis dipanggil oleh browser pada titik-titik tertentu dalam siklus hidup elemen.
💡 Kenapa penting memahami lifecycle?
- Manajemen Sumber Daya: Tahu kapan harus menginisialisasi event listener atau kapan harus membersihkannya (
disconnectedCallback) untuk mencegah memory leak. - Performa: Melakukan pekerjaan berat hanya saat elemen benar-benar siap dan terlihat (
connectedCallback). - Reaktivitas: Merespons perubahan atribut (
attributeChangedCallback) secara efisien. - Debugging: Memahami urutan eksekusi membantu dalam melacak masalah.
- Prediktabilitas: Membangun komponen yang perilakunya bisa diprediksi di berbagai skenario.
3. Mendaftar Custom Element: customElements.define()
Sebelum kita bisa melihat lifecycle-nya, kita perlu mendaftarkan Custom Element kita. Sintaks dasarnya adalah:
customElements.define(name, constructor, options);
name: String nama tag kustom Anda (wajib mengandung tanda hubung, e.g.,my-element,user-card). Ini adalah konvensi untuk membedakannya dari elemen HTML standar.constructor: Class JavaScript yang mendefinisikan elemen Anda, yang harus mewarisi dariHTMLElement.options(opsional): Objek konfigurasi, misalnya{ extends: 'button' }untuk membuat customized built-in element. Untuk artikel ini, kita akan fokus pada autonomous custom elements (elemen yang sepenuhnya baru).
Contoh:
// my-counter.js
class MyCounter extends HTMLElement {
constructor() {
super(); // Wajib! Memanggil constructor HTMLElement
console.log('Counter: Constructor dipanggil');
}
connectedCallback() {
console.log('Counter: connectedCallback dipanggil');
}
disconnectedCallback() {
console.log('Counter: disconnectedCallback dipanggil');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Counter: attributeChangedCallback - Atribut ${name} berubah dari ${oldValue} menjadi ${newValue}`);
}
// Atribut yang ingin dimonitor perubahannya
static get observedAttributes() {
return ['count', 'label'];
}
}
customElements.define('my-counter', MyCounter);
Sekarang, mari kita lihat setiap callback secara detail.
4. Menggali Lifecycle Callbacks Custom Elements
4.1. constructor(): Inisialisasi Awal
✅ Kapan dipanggil: Saat sebuah instance dari Custom Element Anda dibuat. Ini terjadi sebelum elemen terhubung ke DOM. ⚠️ Yang harus diperhatikan:
- Wajib memanggil
super()sebagai baris pertama, karena Custom Element Anda mewarisi dariHTMLElement. - Jangan mengakses atribut atau children dari elemen di sini, karena belum tentu tersedia atau terhubung ke DOM.
- Ideal untuk inisialisasi state internal, membuat Shadow DOM (jika digunakan), atau mengikat event handler yang tidak terikat ke DOM.
class MyElement extends HTMLElement {
constructor() {
super(); // Selalu panggil super()!
console.log('1. Constructor: Elemen baru dibuat.');
this._count = 0; // Inisialisasi state internal
this.attachShadow({ mode: 'open' }); // Buat Shadow DOM
// this.shadowRoot.innerHTML = `<p>Hello from Shadow DOM!</p>`; // Masih OK, tapi sebaiknya di connectedCallback jika butuh atribut
}
// ... callback lainnya ...
}
4.2. connectedCallback(): Saat Elemen Masuk DOM
✅ Kapan dipanggil: Setiap kali instance Custom Element Anda ditambahkan ke dokumen DOM. Ini juga bisa dipanggil beberapa kali jika elemen dipindahkan di DOM. 🎯 Apa yang bisa dilakukan:
- Menyiapkan rendering awal elemen.
- Mengakses atribut elemen (
this.getAttribute()). - Menambahkan event listener ke elemen itu sendiri atau ke elemen lain di Shadow DOM-nya.
- Melakukan fetching data awal.
- Mengatur
innerHTMLataushadowRoot.innerHTML.
class MyElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._count = 0;
}
connectedCallback() {
console.log('2. connectedCallback: Elemen telah ditambahkan ke DOM.');
this.render(); // Panggil metode render
this.shadowRoot.querySelector('button').addEventListener('click', this._increment);
}
render() {
const label = this.getAttribute('label') || 'Counter';
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
</style>
<div>
<p>${label}: <span id="value">${this._count}</span></p>
<button>Increment</button>
</div>
`;
}
_increment = () => { // Gunakan arrow function untuk menjaga konteks `this`
this._count++;
this.shadowRoot.querySelector('#value').textContent = this._count;
}
// ... callback lainnya ...
}
4.3. disconnectedCallback(): Saat Elemen Keluar DOM
✅ Kapan dipanggil: Setiap kali instance Custom Element Anda dihapus dari dokumen DOM, atau saat dokumen tempatnya berada dibongkar (misalnya, saat navigasi halaman). ⚠️ Yang harus diperhatikan:
- Ini adalah tempat krusial untuk membersihkan sumber daya yang telah dialokasikan di
connectedCallback. - Hapus semua event listener yang terpasang ke objek global (seperti
windowataudocument), atau event listener yang terpasang ke elemen lain di luar Shadow DOM Anda, untuk mencegah memory leak. - Batalkan langganan ke observasi atau interval.
class MyElement extends HTMLElement {
// ... constructor dan connectedCallback ...
disconnectedCallback() {
console.log('3. disconnectedCallback: Elemen telah dihapus dari DOM.');
// Hapus event listener yang dipasang di connectedCallback
this.shadowRoot.querySelector('button').removeEventListener('click', this._increment);
// Contoh lain: Hapus listener dari window jika ada
// window.removeEventListener('resize', this._handleResize);
}
// ... callback lainnya ...
}
❌ Kesalahan Umum: Lupa membersihkan event listener atau observer di disconnectedCallback bisa menyebabkan memory leak dan performa buruk seiring waktu.
4.4. attributeChangedCallback(name, oldValue, newValue): Merespon Perubahan Atribut
✅ Kapan dipanggil: Setiap kali salah satu atribut elemen yang dimonitor ditambahkan, dihapus, atau diubah nilainya. 🎯 Apa yang bisa dilakukan:
- Memperbarui UI elemen berdasarkan perubahan atribut.
- Memicu logika bisnis yang relevan dengan atribut.
⚠️ Yang harus diperhatikan:
- Agar callback ini dipanggil, Anda harus mendefinisikan properti
static get observedAttributes()di class Custom Element Anda, yang mengembalikan array nama atribut yang ingin Anda monitor.
class MyElement extends HTMLElement {
// ... constructor, connectedCallback, disconnectedCallback ...
// Wajib mendefinisikan atribut yang ingin dimonitor
static get observedAttributes() {
return ['count', 'label'];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`4. attributeChangedCallback: Atribut '${name}' berubah dari '${oldValue}' menjadi '${newValue}'.`);
if (oldValue === newValue) return; // Hindari update yang tidak perlu
if (name === 'count') {
this._count = parseInt(newValue) || 0;
if (this.shadowRoot) { // Pastikan Shadow DOM sudah ada
this.shadowRoot.querySelector('#value').textContent = this._count;
}
} else if (name === 'label') {
if (this.shadowRoot) {
this.shadowRoot.querySelector('p').firstChild.textContent = `${newValue}: `;
}
}
}
// ... lainnya ...
}
Contoh Penggunaan:
<my-counter count="5" label="Initial Count"></my-counter>
<script>
const counter = document.querySelector('my-counter');
setTimeout(() => {
counter.setAttribute('count', '10'); // Ini akan memicu attributeChangedCallback
counter.setAttribute('label', 'Updated Counter'); // Ini juga
}, 2000);
</script>
4.5. adoptedCallback(): Elemen Pindah Dokumen
✅ Kapan dipanggil: Saat Custom Element dipindahkan ke dokumen baru (misalnya, dari iframe ke dokumen utama, atau sebaliknya).
⚠️ Yang harus diperhatikan:
- Ini adalah callback yang paling jarang digunakan dalam skenario web development sehari-hari.
- Jika Anda memiliki logika yang sangat spesifik terkait dengan dokumen tempat elemen berada, Anda bisa menanganinya di sini.
class MyElement extends HTMLElement {
// ...
adoptedCallback() {
console.log('5. adoptedCallback: Elemen dipindahkan ke dokumen baru.');
}
// ...
}
5. Mengelola State dan Properti Custom Element
Selain atribut, Custom Elements juga bisa memiliki properti JavaScript. Penting untuk memahami perbedaan dan bagaimana mengelolanya.
- Atribut: Selalu berupa string, ada di markup HTML, dan bisa diakses via
getAttribute()/setAttribute(). Perubahannya memicuattributeChangedCallback. - Properti: Bisa berupa tipe data apapun (angka, boolean, objek), diakses via
this.property. Perubahannya tidak secara otomatis memicuattributeChangedCallback.
✅ Best Practice: Gunakan getter dan setter untuk properti yang mencerminkan atribut. Ini akan membuat properti JavaScript tetap sinkron dengan atribut HTML, dan Anda bisa memicu attributeChangedCallback secara manual atau melakukan validasi.
class MyCounter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._count = 0; // State internal
}
// Getter/Setter untuk properti 'count'
get count() {
return this._count;
}
set count(val) {
if (val !== this._count) {
this._count = val;
// Memperbarui atribut HTML untuk sinkronisasi
this.setAttribute('count', val.toString());
// Atau langsung update UI jika tidak ingin memicu attributeChangedCallback lagi
if (this.shadowRoot) {
this.shadowRoot.querySelector('#value').textContent = this._count;
}
}
}
static get observedAttributes() {
return ['count', 'label'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === 'count') {
// Saat atribut 'count' berubah, update properti internal
this._count = parseInt(newValue) || 0;
if (this.shadowRoot) {
this.shadowRoot.querySelector('#value').textContent = this._count;
}
}
// ... logika untuk 'label' ...
}
connectedCallback() {
// Inisialisasi properti dari atribut saat terhubung
if (this.hasAttribute('count')) {
this.count = parseInt(this.getAttribute('count')) || 0;
}
this.render();
this.shadowRoot.querySelector('button').addEventListener('click', this._increment);
}
_increment = () => {
this.count++; // Menggunakan setter 'count'
}
render() {
// ... rendering menggunakan this._count ...
}
// ... disconnectedCallback ...
}
customElements.define('my-counter', MyCounter);
Dengan getter/setter ini, Anda bisa mengubah counter.count = 20; di JavaScript, dan itu akan secara otomatis memperbarui atribut HTML dan UI.
6. Best Practices dan Tips Praktis
- Keep
constructorLightweight: Hanya lakukan inisialisasi esensial. Pekerjaan yang membutuhkan akses DOM atau atribut sebaiknya diconnectedCallback. - Idempotensi
connectedCallback: Ingat,connectedCallbackbisa dipanggil beberapa kali. Pastikan logika di dalamnya aman untuk dieksekusi berulang kali, atau tambahkan flag untuk mencegah eksekusi ganda jika tidak diinginkan (misalnya,if (!this._initialized) { ... this._initialized = true; }). - Selalu Bersihkan di
disconnectedCallback: Ini adalah kunci untuk mencegah memory leak. Bayangkan sebuah aplikasi SPA di mana komponen sering ditambahkan dan dihapus; jika Anda tidak membersihkan, browser akan menumpuk event listener yang tidak terpakai. - Gunakan
static get observedAttributes()dengan Bijak: Hanya monitor atribut yang benar-benar perlu memicu perubahan UI atau logika. Terlalu banyak monitoring bisa membebani performa. - Hindari Perubahan Atribut Rekursif: Dalam
attributeChangedCallback, berhati-hatilah jika Anda mengubah atribut yang sama. Ini bisa menyebabkan loop tak terbatas. Misalnya, jikaattributeChangedCallbackuntuk atributvaluemengubahthis.setAttribute('value', newValue), ini akan memicu callback lagi. Gunakan kondisiif (oldValue === newValue)atau pastikan logika Anda tidak menciptakan loop. - Shadow DOM untuk Enkapsulasi: Manfaatkan Shadow DOM untuk mengisolasi style dan markup komponen Anda dari CSS global dan JavaScript lainnya. Ini membuat komponen Anda lebih robust dan reusable.
- Event Handling: Untuk event di dalam Shadow DOM, gunakan
this.shadowRoot.querySelector(...).addEventListener(...). Untuk event yang perlu “keluar” dari komponen, dispatch Custom Event dari komponen Anda (this.dispatchEvent(new CustomEvent('my-event', { bubbles: true, composed: true }))).
Kesimpulan
Menguasai siklus hidup Custom Elements vanilla adalah fondasi penting untuk membangun Web Components yang kuat, efisien, dan prediktif. Dengan memahami kapan constructor(), connectedCallback(), disconnectedCallback(), dan attributeChangedCallback() dipanggil, Anda memiliki kontrol penuh atas bagaimana komponen Anda berinteraksi dengan DOM dan sumber daya sistem.
Ini bukan hanya tentang menulis kode yang “bekerja”, tetapi menulis kode yang “bekerja dengan baik” — yang performan, bebas memory leak, dan mudah di-maintain. Dengan prinsip-prinsip ini, Anda bisa membangun blok bangunan web yang benar-benar reusable, yang akan melayani Anda dengan baik di berbagai proyek dan framework di masa depan. Selamat membangun Web Components!
🔗 Baca Juga
- Web Components: Membangun Komponen UI yang Reusable dan Framework-Agnostic
- Shadow DOM: Mengisolasi Style dan Markup di Web Components untuk UI yang Konsisten dan Bebas Konflik
- Komunikasi Antar Web Components: Membangun Interaksi yang Efisien dan Fleksibel
- Mengatasi Tantangan Aksesibilitas (A11y) dalam Shadow DOM dan Web Components: Panduan Praktis