Les Callbacks de Cycle de Vie des Composants
Dans le cadre de notre cours "Maîtriser les Web Components : Créez des Composants Réutilisables et Interopérables", cette leçon explore un aspect fondamental de la création de composants robustes et réactifs : les callbacks de cycle de vie. Comprendre et utiliser ces callbacks est essentiel pour gérer le comportement de vos composants à différentes étapes de leur existence dans le DOM.
Introduction aux Callbacks de Cycle de Vie
Imaginez qu'un composant Web soit une entité vivante : il naît, est connecté au monde, interagit, peut changer d'apparence, et finit par disparaître. Les callbacks de cycle de vie sont des méthodes spéciales que vous pouvez définir dans la classe de votre Custom Element pour réagir à ces moments clés de son existence.
Ils vous permettent d'exécuter du code spécifique lorsque :
- Le composant est initialisé.
- Il est attaché au Document Object Model (DOM).
- Il est détaché du DOM.
- Ses attributs HTML sont modifiés.
- Il est déplacé vers un nouveau document.
En utilisant ces points d'ancrage, vous pouvez implémenter une logique propre à chaque étape, assurant ainsi que votre composant se comporte correctement et gère ses ressources de manière efficace.
Les Principaux Callbacks de Cycle de Vie
Il existe cinq callbacks de cycle de vie principaux pour les Custom Elements, chacun ayant un rôle bien défini.
1. constructor()
Le constructor() est la toute première méthode appelée lorsque votre composant est instancié. C'est l'endroit idéal pour effectuer une initialisation de base qui ne dépend pas de la présence du composant dans le DOM.
- Quand il est appelé : Immédiatement après la création de l'instance du composant, avant même qu'il ne soit attaché au document.
- Objectif : Initialiser les propriétés privées, configurer le Shadow DOM (mais ne pas y ajouter de contenu si celui-ci dépend de l'état global du DOM), et mettre en place des configurations initiales.
- Règles importantes :
- Vous devez appeler
super()en premier, car votre classe étendHTMLElement. Sans cela,thisne sera pas défini. - Évitez toute manipulation du DOM qui nécessite que le composant soit déjà connecté au document (par exemple, calculer des dimensions basées sur le CSS ou interagir avec des éléments parents/enfants non encore attachés).
- N'accédez pas aux attributs du composant ici, car ils pourraient ne pas être encore définis ou prêts.
- Vous devez appeler
class MonComposant extends HTMLElement {
constructor() {
super(); // TOUJOURS appeler super() en premier!
console.log("1. Le constructeur est appelé.");
this._compteur = 0; // Initialisation d'une propriété interne
// this.attachShadow({ mode: 'open' }); // Le Shadow DOM peut être attaché ici
}
// ... autres callbacks
}
2. connectedCallback()
Le connectedCallback() est l'un des callbacks les plus utilisés. Il est appelé chaque fois que le composant est attaché au DOM du document.
- Quand il est appelé :
- La première fois que le composant est inséré dans le DOM.
- Chaque fois qu'il est réinséré après avoir été détaché (par exemple, en le déplaçant d'un parent à un autre).
- Objectif :
- Effectuer le rendu initial du Shadow DOM (si le contenu dépend des attributs ou de la position dans le DOM).
- Récupérer des données initiales via des appels API.
- Mettre en place des écouteurs d'événements (event listeners) sur le composant lui-même ou sur le
window/document. - Accéder à d'autres éléments du DOM.
- Précautions : Puisqu'il peut être appelé plusieurs fois, assurez-vous que le code à l'intérieur est idempotent (produit le même résultat si appelé plusieurs fois avec les mêmes entrées) ou qu'il gère les réinsertions correctement (par exemple, en ne recréant pas des écouteurs d'événements à chaque fois).
class MonComposant extends HTMLElement {
constructor() {
super();
// Initialisation minimaliste
}
connectedCallback() {
console.log("2. Le composant est connecté au DOM.");
if (!this.shadowRoot) { // S'assurer que le Shadow DOM est créé une seule fois
this.attachShadow({ mode: 'open' });
this.render(); // Méthode pour générer le contenu
}
// Ajouter un écouteur d'événements sur le bouton par exemple
const button = this.shadowRoot.querySelector('button');
if (button) {
this._clickHandler = this._handleClick.bind(this);
button.addEventListener('click', this._clickHandler);
}
// Lancer un fetch de données
// fetch('api/data').then(response => ...);
}
// Méthode de rendu (exemple)
render() {
this.shadowRoot.innerHTML = `
<style>
button { padding: 10px; }
</style>
<button>Cliquez-moi</button>
<p>Compteur: <span id="compteur">0</span></p>
`;
}
_handleClick() {
this._compteur++;
this.shadowRoot.getElementById('compteur').textContent = this._compteur;
console.log(`Compteur mis à jour : ${this._compteur}`);
}
}
3. disconnectedCallback()
Le disconnectedCallback() est l'opposé de connectedCallback(). Il est appelé lorsque le composant est retiré du DOM.
- Quand il est appelé :
- Lorsque le composant est retiré directement du DOM (ex:
element.remove(),parentNode.removeChild(element)). - Lorsque l'utilisateur navigue vers une autre page, ce qui entraîne la destruction du document actuel.
- Lorsque le composant est retiré directement du DOM (ex:
- Objectif : Effectuer des opérations de nettoyage pour éviter les fuites de mémoire et libérer les ressources.
- Supprimer les écouteurs d'événements globaux (ceux attachés à
window,document, ou d'autres éléments externes). - Annuler les timers (
setTimeout,setInterval). - Annuler les requêtes réseau en cours (
AbortController).
- Supprimer les écouteurs d'événements globaux (ceux attachés à
- Importance : Ignorer le nettoyage peut entraîner des performances dégradées et des fuites de mémoire, surtout pour les applications à longue durée de vie.
class MonComposant extends HTMLElement {
// ... constructor, connectedCallback, etc.
disconnectedCallback() {
console.log("3. Le composant est déconnecté du DOM.");
// Nettoyer les écouteurs d'événements si attachés
if (this._clickHandler) {
const button = this.shadowRoot.querySelector('button');
if (button) {
button.removeEventListener('click', this._clickHandler);
}
this._clickHandler = null; // Libérer la référence
}
// clearInterval(this._myInterval); // Si un intervalle était défini
// this._abortController.abort(); // Si une requête fetch était en cours
}
}
4. attributeChangedCallback(name, oldValue, newValue)
Ce callback est appelé chaque fois qu'un des attributs observés du composant est ajouté, supprimé ou modifié.
- Quand il est appelé : Après que l'attribut HTML correspondant a été modifié.
- Paramètres :
name: Le nom de l'attribut qui a changé.oldValue: L'ancienne valeur de l'attribut (seranullsi l'attribut a été ajouté).newValue: La nouvelle valeur de l'attribut (seranullsi l'attribut a été supprimé).
- Nécessite
static get observedAttributes(): Pour queattributeChangedCallbacksoit appelé, vous devez définir une propriété statiqueobservedAttributesqui retourne un tableau des noms d'attributs que votre composant souhaite observer. Sans cette liste, aucune modification d'attribut ne déclenchera ce callback. - Objectif : Mettre à jour la logique interne ou le rendu du composant en fonction des changements de ses attributs.
class MonComposantAttributs extends HTMLElement {
static get observedAttributes() {
return ['couleur', 'taille']; // Attributs à observer
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
div { padding: 10px; border: 1px solid black; }
</style>
<div id="content"></div>
`;
}
connectedCallback() {
console.log("Composant attributs connecté.");
this._updateContent(); // Mettre à jour le contenu initial
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`4. L'attribut '${name}' a changé de '${oldValue}' à '${newValue}'.`);
// Réagir au changement d'attribut
if (oldValue !== newValue) { // Éviter les mises à jour inutiles
this._updateContent();
}
}
_updateContent() {
const couleur = this.getAttribute('couleur') || 'black';
const taille = this.getAttribute('taille') || 'medium';
const contentDiv = this.shadowRoot.getElementById('content');
if (contentDiv) {
contentDiv.style.color = couleur;
contentDiv.style.fontSize = this._getFontSize(taille);
contentDiv.textContent = `Je suis un texte en ${couleur} et de taille ${taille}.`;
}
}
_getFontSize(taille) {
switch (taille) {
case 'small': return '12px';
case 'medium': return '16px';
case 'large': return '24px';
default: return '16px';
}
}
}
5. adoptedCallback(oldDoc, newDoc)
Ce callback est le moins fréquemment utilisé. Il est appelé lorsque le composant est déplacé d'un document à un autre (par exemple, entre des iframes).
- Quand il est appelé : Lorsque le nœud du composant est "adopté" par un nouveau document via
document.adoptNode(). - Paramètres :
oldDoc: Le document d'origine.newDoc: Le nouveau document.
- Objectif : Mettre à jour des références internes ou des contextes qui pourraient être liés au document d'origine.
- Rareté : Dans la plupart des applications Web classiques, ce scénario est peu courant. Il est plus pertinent dans des contextes complexes impliquant plusieurs documents ou des manipulations avancées d'iframes.
class MonComposantAdopted extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<p>Je suis dans ${document.title}</p>`;
}
adoptedCallback(oldDocument, newDocument) {
console.log(`5. Le composant a été adopté par un nouveau document.`);
console.log(` De: '${oldDocument.title}' vers: '${newDocument.title}'.`);
// Mettre à jour des références spécifiques au document si nécessaire
this.shadowRoot.querySelector('p').textContent = `Je suis maintenant dans ${newDocument.title}`;
}
}
Exemple Pratique Complet : Un Compteur de Clics Réactif
Créons un composant simple qui affiche un compteur de clics et réagit à un attribut initial-value.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exemple de Callbacks de Cycle de Vie</title>
</head>
<body>
<h1>Mon Compteur Personnalisé</h1>
<!-- Exemple d'utilisation du composant -->
<my-compteur initial-value="5"></my-compteur>
<my-compteur></my-compteur>
<button id="removeBtn">Supprimer le premier compteur</button>
<button id="addBtn">Ajouter un nouveau compteur</button>
<script>
class MyCompteur extends HTMLElement {
// 1. Attributs observés pour attributeChangedCallback
static get observedAttributes() {
return ['initial-value'];
}
// 2. constructor() - Initialisation de base
constructor() {
super();
console.log('👷 Constructor: Composant instancié.');
this._count = 0; // Valeur par défaut
this._clickHandler = this._handleClick.bind(this); // Liaison du contexte pour l'écouteur
// Création du Shadow DOM
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
background-color: #f9f9f9;
}
button {
padding: 8px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background-color: #0056b3;
}
span {
font-weight: bold;
color: #333;
margin-left: 10px;
}
</style>
<button id="incrementButton">Cliquez-moi</button>
<span>Compteur: <span id="currentCount">0</span></span>
`;
}
// 3. connectedCallback() - Lorsque le composant est attaché au DOM
connectedCallback() {
console.log('🟢 connectedCallback: Composant connecté au DOM.');
this._updateDisplay(); // Afficher la valeur initiale/courante
const initialValue = parseInt(this.getAttribute('initial-value'), 10);
if (!isNaN(initialValue) && this._count === 0) { // S'assurer que cela ne se réinitialise pas à chaque reconnexion si déjà incrémenté
this._count = initialValue;
this._updateDisplay();
}
// Ajout de l'écouteur d'événement
const button = this.shadowRoot.getElementById('incrementButton');
if (button) {
button.addEventListener('click', this._clickHandler);
}
}
// 4. disconnectedCallback() - Lorsque le composant est détaché du DOM
disconnectedCallback() {
console.log('🔴 disconnectedCallback: Composant déconnecté du DOM.');
// Nettoyage de l'écouteur d'événement pour éviter les fuites de mémoire
const button = this.shadowRoot.getElementById('incrementButton');
if (button) {
button.removeEventListener('click', this._clickHandler);
}
}
// 5. attributeChangedCallback() - Lorsque les attributs observés changent
attributeChangedCallback(name, oldValue, newValue) {
console.log(`⚙️ attributeChangedCallback: Attribut '${name}' changé de '${oldValue}' à '${newValue}'.`);
if (name === 'initial-value') {
const newInitialValue = parseInt(newValue, 10);
if (!isNaN(newInitialValue)) {
this._count = newInitialValue; // Met à jour la valeur interne
this._updateDisplay(); // Met à jour l'affichage
}
}
}
// adoptedCallback() - Moins courant, pas implémenté ici pour simplicité
// Méthode interne pour gérer le clic
_handleClick() {
this._count++;
console.log(`Click! Nouvelle valeur: ${this._count}`);
this._updateDisplay();
}
// Méthode interne pour mettre à jour l'affichage
_updateDisplay() {
const countSpan = this.shadowRoot.getElementById('currentCount');
if (countSpan) {
countSpan.textContent = this._count;
}
}
}
// Définir le custom element
customElements.define('my-compteur', MyCompteur);
// Interaction avec les boutons pour tester les callbacks
const removeBtn = document.getElementById('removeBtn');
const addBtn = document.getElementById('addBtn');
const firstCompteur = document.querySelector('my-compteur');
const body = document.body;
removeBtn.addEventListener('click', () => {
if (firstCompteur && firstCompteur.parentNode) {
firstCompteur.parentNode.removeChild(firstCompteur);
}
});
addBtn.addEventListener('click', () => {
const newCompteur = document.createElement('my-compteur');
newCompteur.setAttribute('initial-value', Math.floor(Math.random() * 100));
body.appendChild(newCompteur);
});
// Tester le changement d'attribut après un délai
setTimeout(() => {
if (firstCompteur) {
firstCompteur.setAttribute('initial-value', '10');
}
}, 3000); // Change l'attribut après 3 secondes
</script>
</body>
</html>
Explication du Code :
- HTML Statique : Nous avons deux instances de
<my-compteur>, l'une avec uninitial-valueet l'autre sans. Des boutons sont ajoutés pour tester la suppression et l'ajout dynamique de composants. static get observedAttributes(): Nous déclarons que notre composant souhaite être notifié des changements de l'attributinitial-value.constructor():super()est appelé en premier._countest initialisé à 0._clickHandlerest lié à l'instance pour s'assurer quethisfait référence au composant à l'intérieur de la méthode_handleClick.- Le Shadow DOM est attaché et son contenu initial (bouton et affichage du compteur) est défini.
connectedCallback():- Ce callback est appelé lorsque le composant est inséré dans le DOM.
- Nous appelons
_updateDisplay()pour afficher la valeur initiale. - Nous vérifions l'attribut
initial-valueet mettons à jour_countsi une valeur valide est trouvée. La conditionthis._count === 0permet d'éviter de réinitialiser le compteur si l'utilisateur a déjà cliqué dessus et que le composant est juste reconnecté. - L'écouteur d'événement pour le bouton est ajouté ici, car le bouton est maintenant disponible dans le Shadow DOM.
disconnectedCallback():- Lorsque le composant est retiré du DOM (en cliquant sur "Supprimer le premier compteur"), ce callback est appelé.
- L'écouteur d'événement ajouté dans
connectedCallbackest supprimé pour éviter les fuites de mémoire. C'est crucial pour la propreté de votre code.
attributeChangedCallback():- Lorsqu'une instance de
my-compteurest ajoutée avecinitial-value="5", ce callback est appelé. - De même, lorsque le
setTimeoutaprès 3 secondes modifie l'attributinitial-valuedu premier composant, ce callback est également appelé. - Il vérifie le nom de l'attribut et met à jour
_countet l'affichage en conséquence.
- Lorsqu'une instance de
- Méthodes Internes (
_handleClick,_updateDisplay) : Ces méthodes gèrent la logique du composant et la mise à jour de son interface utilisateur.
Cet exemple montre comment les différents callbacks s'interconnectent pour gérer le cycle de vie complet d'un composant, de son initialisation à sa suppression, en passant par les interactions et les changements d'attributs.
Bonnes Pratiques et Considérations
- Minimisez le travail dans
constructor(): Gardez-le léger et limitez-le à l'initialisation des propriétés et à la mise en place du Shadow DOM. Les opérations coûteuses ou dépendantes du DOM doivent être placées dansconnectedCallback(). - Idempotence de
connectedCallback(): PuisqueconnectedCallback()peut être appelé plusieurs fois (si le composant est détaché puis réattaché), assurez-vous que votre code gère cela correctement. Par exemple, créez le Shadow DOM une seule fois ou vérifiez si des écouteurs d'événements sont déjà attachés. - Nettoyage dans
disconnectedCallback(): C'est une erreur courante d'oublier de nettoyer les ressources dansdisconnectedCallback(). Cela peut entraîner des fuites de mémoire et des bugs difficiles à déboguer. - Performance de
attributeChangedCallback(): Ce callback peut être appelé fréquemment si de nombreux attributs observés changent. Optimisez votre logique à l'intérieur pour éviter des rendus inutiles. ComparezoldValueetnewValueavant d'effectuer des mises à jour coûteuses. - Préférez les propriétés aux attributs pour l'état interne : Les attributs sont des chaînes de caractères et sont exposés au DOM. Pour l'état interne et les données non exposées, utilisez des propriétés JavaScript standard sur votre instance de classe (
this._myInternalState = ...). Si vous voulez qu'une propriété puisse être définie via HTML, utilisez un accesseur (setter) qui met à jour l'attribut, et laissezattributeChangedCallbackréagir à l'attribut.
Conclusion
Les callbacks de cycle de vie sont la pierre angulaire des Custom Elements bien conçus. Ils vous offrent le contrôle précis nécessaire pour :
- Initialiser vos composants de manière appropriée.
- Gérer leurs ressources lorsqu'ils sont connectés ou déconnectés du DOM.
- Réagir dynamiquement aux changements de leurs attributs.
En maîtrisant ces points d'ancrage, vous pouvez créer des Web Components non seulement fonctionnels, mais aussi performants, réutilisables et résilients, capables de s'intégrer harmonieusement dans n'importe quelle application Web. Prenez le temps de comprendre quand chaque callback est déclenché et quel type de logique doit y être implémenté pour construire des composants robustes et maintenables.