Cycle de vie des composants

Introduction

Un composant Vue traverse plusieurs étapes successives tout au long de son existence. Il passe par une phase de création, d'affichage, de mise à jour lorsque ses données changent, puis de retrait du DOM lorsque Vue n'en a plus besoin.

À chacune de ces étapes, Vue propose des fonctions spéciales appelées hooks (ou accroches du cycle de vie). Ces hooks permettent d'exécuter du code exactement au moment où une étape se produit, afin de préparer le composant, interagir avec le DOM une fois qu'il est visible, ou libérer les ressources lorsqu'il est retiré.

Les hooks servent de repères dans le temps. Ils permettent d'exécuter certaines actions au bon moment lorsqu'un composant apparaît, se met à jour ou disparaît. On les utilise souvent pour initialiser une interface, synchroniser une intégration externe ou effectuer un nettoyage.

Les hooks du cycle de vie se distinguent des observateurs, mais les complètent. Un observateur réagit aux changements de données tandis qu'un hook réagit aux moments clés de la vie du composant.

Déroulement du cycle de vie d'un composant

Le cycle de vie d'un composant Vue se déroule selon un ordre précis d'étapes internes. Chacune correspond à un moment clé où Vue prépare, affiche, met à jour ou retire le composant. Les points ci-dessous décrivent ces étapes dans leur ordre d'exécution.

  1. Initialisation du composant (en mémoire, avant le montage) 
    • Vue instancie le composant en mémoire et lit sa définition (props, données, template…).
    • Les propriétés réactives sont créées et connectées au système de réactivité interne.
    • Tout est prêt pour générer un premier rendu, mais rien n'est encore affiché à l'écran.
  2. Avant le montage du composant (onBeforeMount()) 
    • Vue prépare le rendu du composant afin de l'ajouter dans le DOM réel.
    • Les éléments du DOM cible sont analysés et prêts à recevoir le contenu généré par Vue.
    • Aucun élément du composant n'est encore visible.
  3. Montage du composant (onMounted()) 
    • Le DOM virtuel est transformé en éléments HTML réels et inséré dans la page.
    • Les directives (v-if, v-for, v-model, etc.) et événements du template deviennent actifs.
    • Le composant est désormais visible et fonctionnel, et il est maintenant possible d'interagir avec le DOM.
  4. Avant la mise à jour du composant (onBeforeUpdate()) 
    • Déclenché lorsqu'une donnée réactive utilisée dans le rendu est modifiée (ref, reactive, computed).
    • Vue compare l'ancienne et la nouvelle version du DOM virtuel (diffing) pour déterminer les modifications à appliquer.
    • C'est le moment juste avant que le DOM réel ne soit mis à jour.
  5. Après la mise à jour du composant (onUpdated()) 
    • Le DOM réel a été synchronisé avec les nouvelles données réactives.
    • Toutes les modifications visibles sont terminées et il est maintenant possible d'interagir avec le DOM actualisé.
    • Ce hook est déclenché même si la mise à jour provient d'un composant enfant.
  6. Avant le démontage du composant (onBeforeUnmount()) 
    • Le composant s'apprête à être retiré du DOM, mais les événements sont encore actifs.
    • C'est le bon moment pour nettoyer les ressources détectées comme externes (timers, écouteurs globaux…).
  7. Après le démontage du composant (onUnmounted()) 
    • Vue supprime les écouteurs d'événements créés automatiquement.
    • Les références réactives et watchers internes sont libérés.
    • Le DOM virtuel associé est détruit et retiré du suivi de Vue.
    • Le lien avec les composants enfants est rompu.

Il est important de noter que les hooks d'actualisation (onBeforeUpdate et onUpdated) d'un composant peuvent se déclencher lorsqu'une donnée réactive utilisée dans son rendu est mise à jour, mais aussi lorsqu'un composant enfant effectue sa propre mise à jour. Dans ce second cas, le composant parent est réactualisé même s'il n'a modifié aucune de ses données réactives, car une partie de son rendu dépend, directement ou indirectement, du rendu de ses enfants.

BeforeOnMount() et onMounted() : Les hooks liés au montage du composant

Les hooks d'actualisation (onBeforeUpdate() et onUpdated()) sont utiles pour comprendre le cycle de mise à jour d'un composant, mais ils sont moins utilisés en pratique que onMounted() ou onUnmounted(). Dans la plupart des projets métier, on manipule peu directement le DOM rendu, car les données réactives, les valeurs calculées et le rendu conditionnel couvrent déjà l'essentiel des besoins. Les exemples fournis ici servent avant tout d'illustrations pédagogiques sur la façon dont Vue gère une mise à jour, plutôt que de modèles à appliquer systématiquement.

BeforeOnMount() : agir avant le montage du composant

La fonction onBeforeMount() s'exécute juste avant que le composant ne soit inséré dans le DOM. À ce moment, le rendu virtuel du composant est prêt, mais aucun élément du template n'existe encore dans la page. C'est l'endroit idéal pour effectuer des préparations qui doivent être terminées avant l'affichage, comme le chargement d'une configuration ou la restauration d'une préférence enregistrée.

Exemple : restaurer une préférence avant l'affichage

Dans cet exemple, on souhaite appliquer automatiquement le thème choisi par l'utilisateur avant que le composant ne soit affiché. La fonction onBeforeMount() permet d'exécuter cette vérification au bon moment, afin que la page s'affiche directement avec le bon thème sans provoquer de changement visuel après le rendu.


                <script setup>
                import { ref, onBeforeMount } from 'vue';

                // Thème du site (par défaut clair).
                const themeSombre = ref(false);

                // Bascule le thème et enregistre la préférence.
                const basculerTheme = () =>
                {
                    // Inverser la valeur booléenne.
                    themeSombre.value = !themeSombre.value;

                    // Enregistrer la valeur 'sombre' ou 'clair' dans le localStorage,
                    // sous la clé 'theme', selon la valeur actuelle de 'themeSombre.value'.
                    //
                    // Ici, themeSombre ne change que via la fonction basculerTheme(), donc écrire
                    // dans le localStorage et modifier le <body> directement dans cette fonction suffit.
                    //
                    // Si themeSombre pouvait être modifié ailleurs, 
                    // il faudrait placer les deux actions suivantes dans un observateur (watch).
                    // Cela permettrait de garder l'interface et le stockage toujours synchronisés.
                    localStorage.setItem('theme', themeSombre.value ? 'sombre' : 'clair');

                    // Pour un élément du DOM qui se trouve dans le template d'un composant,
                    // on utilise normalement une ref Vue. Cela permet à Vue de gérer l'élément
                    // dans son propre cycle de vie.
                    //
                    // Le <body> n'appartient à aucun composant. Il fait partie de la page globale,
                    // tout comme <html>, <head> ou encore les objets window et document.
                    // Vue ne peut donc pas créer de ref pour ces éléments.
                    // Dans ce cas particulier, il est normal d'utiliser les outils natifs du navigateur
                    // (document.body, document.querySelector, window, etc.).
                    document.body.classList.toggle('sombre', themeSombre.value);
                };

                // Exécuté avant le montage du composant.
                onBeforeMount(() =>
                {
                    // Restaurer la préférence de thème enregistrée, si elle existe.
                    const sauvegarde = localStorage.getItem('theme');
                    if (sauvegarde === 'sombre')
                    {
                        themeSombre.value = true;
                        document.body.classList.add('sombre');
                    }
                });
                </script>

                <template>
                    <button @click="basculerTheme">
                        Activer le thème {{ themeSombre ? 'clair' : 'sombre' }}
                    </button>
                </template>

                <style>
                body.sombre {
                    background-color: #222;
                    color: white;
                }
                </style>
            

Notez que Vue ne traite pas les blocs <script> et <template> dans l'ordre où ils apparaissent dans le fichier. Que le bloc <template> soit placé avant ou après le bloc <script> ne change absolument rien au fonctionnement du composant. Vue compile d'abord entièrement le composant, puis déclenche onBeforeMount() juste avant que le contenu ne soit inséré dans le DOM. L'ordre des blocs n'a donc aucune incidence sur ce comportement.

OnMounted() : agir une fois le composant monté

La fonction onMounted() se déclenche au moment où le composant vient d'être installé dans le DOM. À cet instant les éléments du template existent vraiment dans la page et les références de template pointent vers de vrais noeuds HTML.

Exemple : lire une ref après le montage

L'exemple suivant affiche un bloc dont on souhaite récupérer la hauteur réelle afin de l'afficher à l'écran. On utilise une référence sur la balise du template et on attend que le composant soit affiché pour être certain que l'élément existe dans le DOM avant de le mesurer.


                <script setup>
                import { ref, onMounted } from 'vue';

                // Référence vers un élément du DOM.
                const boite = ref(null);

                // Hauteur affichée de l'élément (non connue avant affichage).
                const hauteur = ref(0);

                // La fonction passée à "onMounted()" est exécutée uniquement
                // lorsque le composant est affiché à l'écran et que les éléments du template
                // existent réellement dans le DOM.
                onMounted(() =>
                {
                    // Si on essayait d'accéder à "boite.value" sans passer par "onMounted()",
                    // la référence pointerait encore sur null, car le composant n'aurait pas
                    // encore été ajouté au DOM réel.
                    hauteur.value = boite.value.offsetHeight;
                });
                </script>

                <template>
                    <!--
                        On relie cette balise <div> à une ref Vue nommée "boite".
                        Cette ref permettra d'accéder à l'élément du DOM dans la partie <script>.
                    -->
                    <div ref="boite" style="padding:20px; background:#eef">
                        Je suis un bloc dont on mesure la hauteur
                    </div>

                    <p>Hauteur actuelle : {{ hauteur }} px</p>
                </template>
            

Grâce à la fonction onMounted() Vue garantit que tous les éléments du template ont été insérés dans le DOM avant d'exécuter la mesure. Sans l'utilisation de ce hook, il y aurait de fortes chances que la référence soit encore vide au moment où le script tente de lire ses propriétés ce qui empêcherait toute mesure cohérente (boite.value vaudrait null).

onBeforeUpdate() et onUpdated() : Les hooks liés aux mises à jour du composant

onBeforeUpdate() : agir juste avant la mise à jour du DOM

La fonction onBeforeUpdate() s'exécute juste avant que Vue ne réactualise le DOM du composant. C'est l'endroit idéal pour préparer une action avant le rendu, sauvegarder un état ou éviter des effets indésirables liés à la mise à jour à venir.

Exemple : navigation clavier avec tableau de refs

Dans l'exemple ci-dessous, une liste de tâches se parcourt au clavier avec les flèches. La ligne sélectionnée peut être supprimée avec la touche Entrée. Un tableau de refs conserve les éléments <li> pour pouvoir faire défiler la liste vers l'élément sélectionné. Le hook onBeforeUpdate() vide ce tableau juste avant chaque nouveau rendu afin d'éviter de garder des références vers des éléments supprimés.


            <script setup>
            import { ref, onBeforeUpdate } from 'vue';

            const items = ref([
                { id: 1, label: 'Tâche 1' },
                { id: 2, label: 'Tâche 2' },
                { id: 3, label: 'Tâche 3' },
                { id: 4, label: 'Tâche 4' },
                { id: 5, label: 'Tâche 5' },
                { id: 6, label: 'Tâche 6' },
                { id: 7, label: 'Tâche 7' },
                { id: 8, label: 'Tâche 8' }
            ]);

            // Index de l'élément actuellement sélectionné dans la liste.
            const indexSelectionne = ref(0);

            // Tableau de refs vers <li>.
            const itemRefs = ref([]);

            // Appelée pour chaque <li> grâce à :ref.
            // Le tableau reflète les éléments DOM affichés.
            const enregistrerRefElement = (el) => 
            {
                if (el) 
                {
                    itemRefs.value.push(el);
                }
            };

            // Fait défiler la liste pour garder l'élément sélectionné visible.
            const defilerVersElementSelectionne = () => 
            {
                const el = itemRefs.value[indexSelectionne.value];
                if (el) 
                {
                    // scrollIntoView()
                    //    Méthode du navigateur fournie par le DOM qui fait défiler
                    //    la zone de scroll jusqu'à ce que l'élément soit visible.
                    //
                    // { block: 'nearest' } 
                    //    demande au navigateur d'afficher l'élément
                    //    en haut ou en bas de la zone visible en choisissant
                    //    l'option qui nécessite le moins de défilement.
                    el.scrollIntoView({ block: 'nearest' });
                }
            };

            // Gestion du clavier sur le conteneur.
            const gererAppuiTouche = (event) =>
            {
                if (event.key === 'ArrowDown') 
                {
                    event.preventDefault();
                    if (indexSelectionne.value < items.value.length - 1) 
                    {
                        indexSelectionne.value++;
                        defilerVersElementSelectionne();
                    }
                } 
                else if (event.key === 'ArrowUp') 
                {
                    event.preventDefault();
                    if (indexSelectionne.value > 0) 
                    {
                        indexSelectionne.value--;
                        defilerVersElementSelectionne();
                    }
                } 
                else if (event.key === 'Enter') 
                {
                    event.preventDefault();
                    if (!items.value.length) return;

                    // Supprimer l'élément sélectionné.
                    items.value.splice(indexSelectionne.value, 1);

                    // Recadrer l'index si on a supprimé le dernier.
                    if (indexSelectionne.value >= items.value.length) 
                    {
                        indexSelectionne.value = items.value.length - 1;
                    }
                }
            }

            // Avant chaque nouveau rendu, on vide le tableau de refs
            // pour ne pas conserver d'éléments supprimés.
            onBeforeUpdate(() => 
            {
                itemRefs.value = [];
            });
            </script>

            <template>
                <div>
                    <ul>
                        <li>Cliquer dans la liste puis utiliser les flèches haut et bas.</li>
                        <li>Entrée supprime la ligne sélectionnée et la liste de tâches se met à jour.</li>
                    </ul>

                    <div 
                        tabindex="0" 
                        @keydown="gererAppuiTouche"
                        class="liste-clavier"
                    >
                        <ul class="liste-clavier__liste">
                            <li 
                                v-for="(item, index) in items" 
                                :key="item.id" 
                                :ref="enregistrerRefElement"
                                class="liste-clavier__item"
                                :class="{ selected: index === indexSelectionne }"
                            >
                                {{ item.label }}
                            </li>
                        </ul>
                    </div>
                </div>
            </template>

            <style scoped>
            .liste-clavier {
                border: 1px solid #ccc;
                max-height: 140px;
                overflow-y: auto;
                padding: 4px;
            }

            .liste-clavier__liste {
                list-style: none;
                padding: 0;
                margin: 0;
            }

            .liste-clavier__item {
                padding: 4px 8px;
                margin-bottom: 2px;
                cursor: default;
            }

            .selected {
                background: #e0f0ff;
            }
            </style>
        

À chaque rendu, Vue rappelle la fonction passée à :ref pour chaque ligne de la liste. Grâce à onBeforeUpdate(), le tableau itemRefs est vidé juste avant la mise à jour puis rempli à nouveau lors du rendu suivant. Il reflète ainsi uniquement les éléments encore présents dans le DOM après la mise à jour, ce qui maintient un lien clair entre l'index sélectionné et l'élément affiché.

Si la vidange est supprimée, l'exemple continue de fonctionner dans ce cas simple, car l'index sélectionné reste recadré sur les éléments visibles. En revanche, le tableau itemRefs commence à accumuler d'anciennes références DOM qui ne sont plus affichées. Dès que la liste devient plus dynamique (tri, filtrage, insertions au milieu, calculs de hauteurs ou de positions à partir des refs), cette accumulation peut provoquer des comportements difficiles à comprendre. Le hook onBeforeUpdate() est utilisé ici pour garder un miroir du DOM propre à chaque mise à jour et éviter ce type d'incohérence lorsqu'on s'appuie davantage sur le tableau de refs.

onUpdated() : agir juste après la mise à jour du DOM

La fonction onUpdated() s'exécute juste après que Vue a réaffiché le composant. C'est le moment idéal pour manipuler le DOM en fonction de son nouvel état visible, par exemple pour ajuster une position, lancer une animation ou synchroniser un composant externe.

Exemple : faire défiler un fil de messages jusqu'en bas après chaque mise à jour

Dans cet exemple, un petit système de messagerie ajoute un message à chaque clic. Après chaque ajout, Vue réactualise la liste, puis le hook onUpdated() défile automatiquement jusqu'au dernier message affiché.


            <script setup>
            import { ref, onUpdated } from 'vue';

            // Liste des messages affichés dans le chat.
            const messages = ref([
                'Bienvenue dans le chat'
            ]);

            // Référence vers le conteneur des messages.
            const conteneur = ref(null);

            // Ajoute un nouveau message à la liste.
            const ajouterMessage = () =>
            {
                messages.value.push('Nouveau message #' + (messages.value.length + 1));
            };

            // onUpdated() s'exécute après chaque réactualisation du DOM par Vue.
            onUpdated(() =>
            {
                // À ce stade, le nouveau message est déjà présent dans le DOM.
                // On force alors le défilement du conteneur vers le bas
                // pour que le dernier message soit toujours visible.
                //
                // scrollHeight correspond à la hauteur totale du contenu,
                // même la partie non visible à l'écran.
                conteneur.value.scrollTop = conteneur.value.scrollHeight;
            });
            </script>

            <template>
                <div class="chat-demo">
                    <div ref="conteneur" class="messages">
                        <p v-for="m in messages" :key="m">{{ m }}</p>
                    </div>

                    <button @click="ajouterMessage">Ajouter un message</button>
                </div>
            </template>

            <style>
                .chat-demo { border:1px solid #ccc; border-radius:8px; width:260px; padding:8px; }
                .messages {
                    height:120px;
                    overflow-y:auto;
                    border:1px solid #ddd;
                    padding:8px;
                    margin-bottom:8px;
                    background:#fafafa;
                }
                button {
                    padding:6px 10px;
                    border:1px solid #888;
                    border-radius:6px;
                    background:#f5f5f5;
                    cursor:pointer;
                }
            </style>
        

Dans cet exemple, chaque nouveau message est ajouté à la fin du tableau messages. Comme la boucle v-for parcourt le tableau dans l'ordre, le message le plus récent s'affiche donc en bas de la liste dans le template. Le hook onUpdated() garantit que le défilement vers le bas s'effectue après que ce nouveau message a été inséré dans le DOM.

Sans cette instruction de défilement manuel, le nouveau message s'afficherait bien en bas de la liste, mais la position de défilement resterait là où elle se trouvait avant la mise à jour. Si le conteneur n'était pas déjà scrolly jusqu'en bas, le dernier message ne serait pas visible sans intervention de l'utilisateur. Le hook onUpdated() sert ici à synchroniser la position de scroll avec l'état le plus récent de la liste.

Si l'on avait utilisé onBeforeUpdate(), le défilement se serait déclenché avant que le DOM soit réellement mis à jour. Le navigateur n'aurait donc connu que la hauteur correspondant à l'ancien contenu, et le scroll se serait arrêté au niveau de l'avant-dernier message. Ce comportement montre bien la différence entre une action préparatoire avant le rendu et une action de synchronisation après le rendu.

beforeOnUnmount() et onUnmounted() : Les hooks liés à la suppression du composant

onBeforeUnmount() : agir juste avant la suppression du composant

La fonction onBeforeUnmount() s'exécute juste avant que le DOM du composant ne soit retiré et que Vue ne désactive la réactivité liée à son rendu. C'est l'endroit idéal pour exécuter une dernière action avant la destruction du composant.

On peut donc s'en servir pour effectuer des opérations qui doivent être interrompues proprement avant la suppression du composant :

Exemple : arrêter un minuteur avant la destruction du composant

Dans cet exemple, un composant affiche un compteur qui s'incrémente automatiquement toutes les secondes grâce à un setInterval(). Le hook onBeforeUnmount() interrompt ce minuteur juste avant que le composant ne soit détruit, évitant ainsi qu'il continue à tourner en arrière-plan après sa suppression.

Composant enfant App.vue (src/components/Minuteur.vue) :


                <script setup>
                import { ref, onMounted, onBeforeUnmount } from 'vue';

                // Valeur du compteur affichée dans le template.
                const compteur = ref(0);

                // Identifiant du minuteur (retourné par setInterval()).
                let idInterval = null;

                // Démarrer le minuteur au montage du composant.
                onMounted(() =>
                {
                    idInterval = setInterval(() =>
                    {
                        compteur.value++;
                        console.log('compteur incrémenté!');
                    }, 1000);
                });

                // Arrêter le minuteur juste avant la suppression du composant.
                onBeforeUnmount(() =>
                {
                    clearInterval(idInterval);
                    console.log('Minuteur arrêté avant la suppression du composant.');
                });
                </script>

                <template>
                    <div class="timer-demo">
                        <p>Compteur actif : {{ compteur }}</p>
                    </div>
                </template>

                <style>
                    .timer-demo {
                        padding: 16px;
                        border: 1px solid #ccc;
                        border-radius: 8px;
                        width: 220px;
                        text-align: center;
                        background: #fafafa;
                    }
                </style>
            

Composant parent App.vue (src/App.vue) :

Le composant parent permet de monter ou démonter le minuteur à l'aide d'un bouton. Lorsqu'il est supprimé, le hook onBeforeUnmount() s'assure que le processus est proprement interrompu avant la destruction du composant.


                <script setup>
                import { ref } from 'vue';
                import Minuteur from './components/Minuteur.vue';

                const afficherMinuteur = ref(true);
                </script>

                <template>
                    <h1>Tester le hook <span class="commande" role="text">onBeforeUnmount()</span></h1>

                    <ol>
                        <li>
                            Montez le composant enfant <b>EcouteClavier.vue</b> à l'aide du bouton ci-dessous.
                        </li>
                        <li>
                            Observez la valeur du compteur s'incrémenter toutes les secondes sous le bouton
                            ainsi que dans la console.
                        </li>
                        <li>
                            Démontez le composant enfant <b>EcouteClavier.vue</b> à l'aide du même bouton.
                        </li>
                        <li>
                            Observez la console, un message devrait indiquer que le minuteur a été interrompu,
                            et les messages d'actualisation du compteur ne devraient plus apparaître.
                        </li>
                    </ol>

                    <button @click="afficherMinuteur = !afficherMinuteur">
                        {{ afficherMinuteur ? 'Supprimer' : 'Créer' }} le minuteur
                    </button>

                    <Minuteur v-if="afficherMinuteur" />
                </template>

                <style scoped>
                    button {
                        margin-top: 12px;
                        padding: 8px 14px;
                        border: 1px solid #999;
                        border-radius: 6px;
                        background: #f7f7f7;
                        cursor: pointer;
                        font-weight: 500;
                    }
                    button:hover {
                        background: #eee;
                    }
                </style>
            

Ici, le hook onBeforeUnmount() sert à interrompre un processus encore actif (le minuteur) avant que le composant ne soit retiré du DOM. Si l'on oubliait cette étape, le minuteur continuerait à s'exécuter, consommant des ressources et générant des erreurs en essayant de modifier un composant qui n'existe plus.

onUnmounted() : agir une fois le composant supprimé du DOM

La fonction onUnmounted() s'exécute une fois que le composant a été retiré du DOM. Ce moment est idéal pour libérer les ressources ou annuler des effets secondaires créés pendant la vie du composant. On l'utilise par exemple pour retirer des écouteurs d'événements globaux, arrêter des minuteries, annuler des requêtes en attente ou fermer une connexion à un service externe.

Exemple : brancher et débrancher un écouteur global

Dans cet exemple, le composant écoute l'événement clavier keydown sur la fenêtre pour réagir à la touche Échap. Le hook onMounted() ajoute l'écouteur lorsque le composant est inséré dans le DOM, tandis que onUnmounted() le retire automatiquement dès que le composant est détruit. Ce mécanisme empêche le code de continuer à réagir alors que le composant n'est plus présent à l'écran.

Composant enfant Produit.vue (src/components/EcouteClavier.vue) :


                <script setup>
                import { ref, onMounted, onUnmounted } from 'vue';

                // Un Set (introduit en ES6) ressemble à un tableau, mais il a deux particularités :
                //   1. Il ne peut contenir que des valeurs uniques (pas de doublons).
                //   2. L'ordre des éléments n'a pas d'importance.
                // On le crée avec "new Set()" et on l'utilise surtout pour stocker des éléments uniques,
                // comme ici : les touches du clavier actuellement pressées.
                const touchesActives = ref(new Set());

                const gererTouchePressee = (e) =>
                {
                    // La méthode .has() permet de vérifier si la touche est déjà dans le Set.
                    // cette vérification permet d'éviter d'exécuter le reste du code en boucle
                    // tant qu'une touche reste enfoncée (keydown).
                    if (touchesActives.value.has(e.key)) return;

                    // Ajouter la touche au Set avec .add()
                    // Contrairement à un tableau où on utiliserait .push(),
                    // ici l'ajout ne crée jamais de doublon.
                    touchesActives.value.add(e.key);

                    // Ce log sert à vérifier que les écouteurs d'événements sont bien retirés
                    // lorsque le composant est détruit.
                    // Cela peut sembler redondant avec l'affichage équivalent dans le template,
                    // mais il permet de prouver que le v-if du parent ne fait pas qu'invisibiliser
                    // le composant, il le supprime réellement, et le hook onUnmounted()
                    // exécute bien son nettoyage.
                    console.log(`La touche "${e.key}" vient d'être activée !`);
                }

                const gererToucheRelachee = (e) =>
                {
                    // La méthode .delete() supprime une valeur précise du Set.
                    // Si la touche était présente, elle est retirée du Set.
                    touchesActives.value.delete(e.key);
                };

                onMounted(() =>
                {
                    // Ajouter deux écouteurs globaux sur la fenêtre :
                    // un pour la pression des touches et un autre pour leur relâchement.
                    window.addEventListener('keydown', gererTouchePressee);
                    window.addEventListener('keyup', gererToucheRelachee);
                });

                onUnmounted(() =>
                {
                    // Retirer les écouteurs lorsqu'on détruit le composant.
                    window.removeEventListener('keydown', gererTouchePressee);
                    window.removeEventListener('keyup', gererToucheRelachee);
                });
                </script>

                <template>
                    <h2>Appuyez sur une ou plusieurs touches.</h2>

                    <!-- Array.from() transforme le Set en tableau classique pour l'affichage -->
                    <p>Touche(s) pressée(s) : {{ Array.from(touchesActives).join(', ') || 'aucune' }} !</p>
                </template>
            

Composant parent App.vue (src/App.vue) :


                <script setup>
                import { ref } from 'vue';
                import EcouteClavier from './components/EcouteClavier.vue';

                // Booléen utilisé pour monter ou démonter le composant enfant.
                // Lorsqu'il passe à false, Vue supprime complètement le composant du DOM,
                // ce qui déclenche automatiquement le hook onUnmounted() dans le composant enfant.
                const activerEcouteClavier = ref(false);
                </script>

                <template>
                    <h1>Tester le hook <span class="commande" role="text">onUnmounted()</span></h1>

                    <ol>
                        <li>
                            Montez le composant enfant <b>EcouteClavier.vue</b> à l'aide du bouton ci-dessous.
                        </li>
                        <li>
                            Appuyez sur une ou plusieurs touches et constatez qu'elles s'affichent sous le bouton
                            ainsi que dans la console.
                        </li>
                        <li>
                            Démontez le composant enfant <b>EcouteClavier.vue</b> à l'aide du même bouton.
                        </li>
                        <li>
                            Appuyez de nouveau sur une touche afin de vérifier que le composant a bien supprimé l'écouteur d'événement global lié au clavier.
                            Aucun message ne doit alors apparaître dans la console.
                        </li>
                    </ol>

                    <button @click="activerEcouteClavier = !activerEcouteClavier">
                        {{ activerEcouteClavier ? 'Supprimer' : 'Créer' }} l'écoute clavier
                    </button>

                    <!-- Le composant est créé ou détruit dynamiquement -->
                    <EcouteClavier v-if="activerEcouteClavier" />

                    <!-- Message visuel une fois le composant retiré -->
                    <p v-else class="etat">
                        <i>L'écoute clavier est désactivée (aucun écouteur actif).</i>
                    </p>
                </template>

                <style scoped>
                    button {
                        margin-top: 12px;
                        padding: 8px 14px;
                        border: 1px solid #999;
                        border-radius: 6px;
                        background: #f7f7f7;
                        cursor: pointer;
                        font-weight: 500;
                    }
                    button:hover {
                        background: #eee;
                    }
                    .etat {
                        color: #777;
                        font-style: italic;
                        margin-top: 10px;
                    }
                </style>
            

Ici, le hook onUnmounted() garantit que les écouteurs globaux ajoutés au montage sont correctement retirés lorsque le composant est supprimé. Sans ce nettoyage, le navigateur continuerait à déclencher les fonctions associées à chaque pression de touche, même si le composant n'existe plus dans le DOM. Ce type d'oubli peut provoquer des fuites mémoire et des comportements inattendus.

Ce scénario est fréquent lorsque des composants interagissent avec des éléments extérieurs à Vue : l'objet global window, des API tierces, des WebSockets, des minuteries ou encore des écouteurs sur le document. Le hook onUnmounted() assure la bonne fermeture du cycle de vie du composant en supprimant toute dépendance devenue inutile une fois celui-ci retiré du DOM.

nextTick() : exécuter du code juste après une mise à jour ciblée

Qu'est-ce que nextTick() ?

nextTick() n'est pas un hook du cycle de vie. C'est une fonction utilitaire fournie par Vue qui permet d'exécuter du code juste après le prochain rafraîchissement du DOM. Elle ne se déclenche jamais automatiquement et n'agit que lorsqu'on l'appelle. Elle peut être utilisée dans n'importe quel contexte dès qu'il est nécessaire d'attendre la fin du rendu en cours avant d'accéder au DOM réel.

Vue regroupe plusieurs modifications dans une même mise à jour afin d'éviter des rendus successifs inutiles. Appeler nextTick() revient à demander à Vue « exécuter ce code une fois que cette vague de mises à jour aura été appliquée au DOM ». On dispose ainsi d'un point d'accroche précis pour lancer un traitement qui dépend de l'interface réellement à jour.

En quoi nextTick() diffère de onUpdated() ?

onUpdated() est un hook du cycle de vie. Il se déclenche automatiquement après chaque mise à jour du composant, une fois que son DOM (et celui de ses enfants) a été réactualisé, quelle que soit l'origine de la modification. On l'utilise lorsque l'on souhaite réagir systématiquement à toutes les mises à jour de l'interface pour ce composant.

nextTick() est appelé volontairement et seulement lorsque l'on en a besoin. Il cible la prochaine mise à jour du DOM et permet de chaîner un traitement juste après cette réactualisation. La fonction ne remplace pas les hooks mais complète le cycle de vie en offrant un moyen d'attendre précisément la fin d'un rendu sans surveiller en continu toutes les mises à jour du composant.

Quand préférer l'un ou l'autre

  • Utiliser nextTick() lorsqu'une mise à jour précise vient d'être déclenchée et que l'on doit intervenir immédiatement après ce rendu unique. On vise une action ponctuelle liée à un élément particulier du DOM réel. Exemple donner le focus à un champ ajouté via v-if, faire défiler une liste vers un élément ajouté, ou initialiser une librairie qui nécessite que les éléments soient physiquement présents dans la page.
  • Utiliser onUpdated() lorsqu'on souhaite exécuter un traitement après toutes les mises à jour du composant, quelle qu'en soit l'origine. On vise une action systématique déclenchée après chaque rendu du composant. Exemple journaliser les mises à jour, recalculer une hauteur globale ou repositionner un élément après chaque réactualisation de l'interface.

Exemple : Champ qui prend le focus dès son ajout au DOM

Dans l'exemple suivant, un bouton permet d'ajouter ou de retirer un champ facultatif à la demande. Lorsque le champ apparaît, on souhaite lui donner le focus immédiatement pour que l'utilisateur puisse commencer à taper sans étape supplémentaire.


                    <script setup>
                    import { ref, nextTick, onUpdated } from 'vue';

                    const estChampFacultatifActif = ref(false);
                    const champFacultatif = ref(null);
                    const nom = ref('');

                    const basculerChampFacultatif = async () =>
                    {
                        // Bascule l'affichage du champ facultatif.
                        estChampFacultatifActif.value = !estChampFacultatifActif.value;

                        // Ne demander le focus que lorsqu'on vient d'activer le champ.
                        if (estChampFacultatifActif.value)
                        {
                            // Attendre que Vue ait inséré l'input dans le DOM.
                            await nextTick();

                            // Donner le focus si la ref pointe bien vers l'élément.
                            if (champFacultatif.value)
                            {
                                champFacultatif.value.focus();
                            }
                        }
                    };

                    /*
                        Pourquoi pas onUpdated()
                        - onUpdated() se déclenche après TOUTE mise à jour du composant,
                        y compris chaque frappe dans le champ "Nom" via v-model="nom".
                        - Effet indésirable
                        le focus serait repris par le champ facultatif à chaque frappe dans "Nom".
                        - nextTick() fait l'inverse
                        on cible une mise à jour précise  juste après avoir activé le champ.
                    */

                    /*
                    onUpdated(() =>
                    {
                        champFacultatif.value?.focus();
                    });
                    */
                    </script>

                    <template>
                        <button @click="basculerChampFacultatif">
                            {{ estChampFacultatifActif ? 'Supprimer le champ' : 'Ajouter le champ' }}
                        </button>

                        <div v-if="estChampFacultatifActif">
                            <label for="facultatif">Champ facultatif</label>
                            <input id="facultatif" ref="champFacultatif" type="text" />
                        </div>

                        <hr />

                        <div>
                            <label for="nom">Nom</label>
                            <input id="nom" v-model="nom" type="text" />
                        </div>
                    </template>
                

On utilise nextTick() pour garantir que le DOM est bien mis à jour avant de donner le focus. Sans cet appel, la fonction se termine alors que l'élément conditionnel n'a pas encore été inséré dans la page. La référence champFacultatif peut encore valoir null au moment de l'appel et focus() échoue ou ne produit aucun effet visible. nextTick() attend la fin du patch du DOM puis exécute le code au moment où l'élément existe réellement.

On évite onUpdated() dans ce cas. Ce hook se déclenche après chaque mise à jour du composant, y compris à chaque frappe dans le champ nom via v-model. Le focus serait repris en permanence par le champ facultatif et l'expérience deviendrait pénible. nextTick() ne s'exécute qu'une fois, juste après l'activation du champ, ce qui évite de voler le focus lors des mises à jour suivantes.

Une variante consisterait à ajouter un drapeau dans onUpdated() pour ne focaliser qu'une seule fois. Cette approche fonctionne, mais elle complexifie inutilement le flux. nextTick() reste plus clair et mieux localisé à l'action qui crée l'élément.

Plan de test recommandé

Test fonctionnel, version de référence
  1. Cliquez sur le bouton pour ajouter le champ facultatif puis vérifiez que le focus arrive bien dans ce champ.
  2. Saisissez ensuite du texte dans le champ Nom et observez que le focus ne bouge pas.
  3. Aucune erreur ne doit apparaître dans la console.
Test non fonctionnel, version sans nextTick()
  1. Retirez la ligne await nextTick() et conservez l'appel champFacultatif.value.focus().
  2. Cliquez pour ajouter le champ facultatif.
  3. Observez le comportement. Le focus ne se place pas dans le champ facultatif, car l'élément n'existe pas encore dans le DOM au moment de l'appel. La référence champFacultatif vaut encore null, donc l'appel à focus() est ignoré.
Test non fonctionnel, version avec onUpdated()
  1. Retirez la ligne await nextTick() (si ce n'est pas déjà fait) et supprimez également l'appel direct à focus() dans la fonction basculerChampFacultatif.
  2. Décommentez le bloc onUpdated(() => { champFacultatif.value?.focus() }).
  3. Cliquez pour ajouter le champ facultatif.
  4. Placez le curseur dans le champ Nom.
  5. Commencez à taper dans Nom et observez que le focus est repris par le champ facultatif à chaque frappe, car le hook se déclenche à chaque mise à jour du composant (ici le v-model="nom").

Exo-hooks-01 : Utiliser les hooks de cycle de vie avec la todoliste

Cet exercice montre comment les hooks de cycle de vie de Vue permettent de regrouper la logique d'initialisation et certaines interactions avec le navigateur au bon moment. La todoliste utilise déjà la réactivité, les propriétés calculées, les observateurs et le découpage en composants. Vous allez maintenant y ajouter une organisation supplémentaire avec les hooks onMounted() et onUnmounted().

Pré-requis : vous disposez d'une todoliste fonctionnelle avec App.vue comme composant principal, une persistance dans le localStorage (via un ou plusieurs watch()), et un découpage en composants TodoForm.vue, TodoList.vue et TodoItem.vue ou une structure très proche.

Partie A : Initialiser la todoliste avec onMounted()

Objectif : déplacer la logique d'initialisation liée au navigateur (lecture du localStorage) dans une fonction dédiée, puis l'exécuter au moment du montage du composant grâce à onMounted().

Instructions :

  1. Ouvrez App.vue et complétez les imports depuis Vue pour ajouter les hooks de cycle de vie nécessaires. Par exemple :
    
                            import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
                        
    Adaptez cette ligne en fonction des fonctions que vous importez déjà.
  2. Repérez dans App.vue le code qui lit les valeurs dans le localStorage au démarrage. En général, ce bloc :
    • lit CLE_LOCALSTORAGE_TACHES
    • lit CLE_LOCALSTORAGE_PROCHAIN_ID
    • initialise taches et prochainId à partir de ces valeurs
  3. Déplacez cette logique dans une fonction dédiée, déclarée dans la partie <script setup>, juste après la création des données réactives. Par exemple :
    
                            function initialiserDepuisLocalStorage()
                            {
                                // 1. Lire la liste depuis localStorage (CLE_LOCALSTORAGE_TACHES).
                                // 2. Si une valeur existe, la convertir avec JSON.parse()
                                //    et remplir la donnée réactive "taches".
                                // 3. Lire la valeur de CLE_LOCALSTORAGE_PROCHAIN_ID.
                                // 4. Si une valeur existe, l'utiliser pour initialiser "prochainId".
                                // 5. Sinon, conserver les valeurs par défaut définies dans le script.
                            }
                        
    Copiez votre logique existante dans cette fonction sans modifier le comportement.
  4. Juste après cette fonction, ajoutez un hook onMounted() qui l'appelle au moment où le composant est monté dans le navigateur :
    
                            onMounted(() =>
                            {
                                console.log('[App] Composant monté, initialisation de la todoliste.');
    
                                initialiserDepuisLocalStorage();
                            });
                        
    Le hook garantit que la lecture du localStorage se fait lorsque l'application tourne bien dans un environnement navigateur, et non au simple import du fichier.
  5. Vérifiez que le comportement reste identique :
    • si des tâches étaient déjà stockées, elles sont toujours restaurées au chargement de la page
    • si rien n'est enregistré, la todoliste démarre avec vos valeurs par défaut

Partie B : Gérer le focus du champ texte avec un hook et une ref

Objectif : utiliser une ref et onMounted() dans TodoForm.vue pour placer automatiquement le focus dans le champ d'ajout de tâche lorsque le formulaire apparaît.

Instructions :

  1. Ouvrez le composant TodoForm.vue qui contient le formulaire d'ajout de tâche.
  2. Complétez les imports depuis Vue si nécessaire pour ajouter le hook onMounted :
    
                            import { ref, onMounted } from 'vue';
                        
  3. Dans la partie <script setup>, créez une ref qui servira à cibler le champ de saisie dans le template :
    
                            const champSaisie = ref(null);
                        
  4. Reliez cette ref à l'input dans le template en ajoutant un attribut ref sur le champ texte utilisé pour saisir la nouvelle tâche. Par exemple :
    
                            <template>
                                <form @submit.prevent="gererSoumission">
                                    <input
                                        ref="champSaisie"
                                        type="text"
                                        v-model="nouvelleTache"
                                        placeholder="Ajouter une tâche"
                                    >
    
                                    <button type="submit">
                                        Ajouter
                                    </button>
                                </form>
                            </template>
                        
    Le script pourra ainsi accéder directement à l'élément DOM correspondant sans utiliser document.querySelector().
  5. Ajoutez un hook onMounted() pour placer automatiquement le focus dans ce champ lorsque le composant est affiché :
    
                            onMounted(() =>
                            {
                                if (champSaisie.value !== null)
                                {
                                    champSaisie.value.focus();
                                }
                            });
                        
    Le hook garantit que le DOM du composant est prêt avant d'appeler focus().
  6. Actualisez votre application et testez le comportement :
    • au chargement, le champ d'ajout de tâche doit recevoir le focus
    • l'ajout et la persistance des tâches doivent continuer à fonctionner grâce aux watch() existants
    • la console doit afficher le message [App] Composant monté, initialisation de la todoliste. lors du montage de App.vue
  7. Faites le point sur l'organisation actuelle :
    • les computed décrivent des valeurs dérivées
    • les watch() réagissent aux changements de données (par exemple pour la sauvegarde dans le localStorage)
    • le hook onMounted() regroupe les effets liés au cycle de vie du composant qui doivent se produire au moment où le DOM est disponible (initialisation depuis le navigateur, focus, petites interactions DOM)
    Chaque outil garde un rôle clair, ce qui rend la todoliste plus lisible et plus simple à faire évoluer.