WEB-COMPONENTS CUSTOM-ELEMENTS FRONTEND JAVASCRIPT WEB-STANDARDS UI-DEVELOPMENT COMPONENT-ARCHITECTURE BEST-PRACTICES VANILLA-JS SHADOW-DOM BROWSER-API

Menguasai Lifecycle Custom Elements Vanilla: Membangun Web Components yang Robust dan Prediktif

⏱️ 13 menit baca
👨‍💻

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?

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);

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:

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:

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:

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:

⚠️ Yang harus diperhatikan:

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:

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.

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

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