1. Hooks de cycle de vie
Chaque composant Vue passe par une série d'étapes : création, montage dans le DOM, mises à jour, et destruction.
La Composition API expose ces étapes sous forme de fonctions onXxx() appelées dans setup().
Ordre d'exécution
| Hook | Moment | Usage typique |
|---|---|---|
onBeforeMount | Avant insertion dans le DOM | Préparer des données |
onMounted | Après insertion dans le DOM | Fetch, refs DOM, setInterval |
onBeforeUpdate | Avant le re-rendu réactif | Sauvegarder état du DOM |
onUpdated | Après le re-rendu | Accéder au DOM mis à jour |
onBeforeUnmount | Avant destruction | Nettoyages préparatoires |
onUnmounted | Après destruction | clearInterval, removeEventListener |
Exemple complet
<script setup>
import {
ref,
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted
} from 'vue'
const count = ref(0)
onBeforeMount(() => {
console.log('onBeforeMount — DOM pas encore là')
})
onMounted(() => {
console.log('onMounted — DOM disponible !')
// Bon moment pour : fetch, setInterval, refs DOM
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate — count va changer')
})
onUpdated(() => {
console.log('onUpdated — count vient de changer :', count.value)
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount — nettoyage en cours')
})
onUnmounted(() => {
console.log('onUnmounted — composant détruit')
})
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
Pattern : setInterval avec cleanup
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const seconds = ref(0)
let timer = null
onMounted(() => {
timer = setInterval(() => { seconds.value++ }, 1000)
})
onUnmounted(() => {
clearInterval(timer) // INDISPENSABLE — évite les fuites mémoire
})
</script>
onUnmounted pour nettoyer les timers et event listeners — sinon ils continuent à tourner même après destruction du composant (fuite mémoire).
2. watch
watch(source, callback, options?) observe une source réactive spécifique et appelle un callback lorsqu'elle change. Contrairement à watchEffect, le tracking est explicite.
Syntaxe de base
<script setup>
import { ref, watch } from 'vue'
const query = ref('')
watch(query, (newVal, oldVal) => {
console.log(`query: "${oldVal}" → "${newVal}"`)
})
</script>
Option immediate — exécuter au montage
watch(query, (val) => {
fetchResults(val)
}, { immediate: true }) // s'exécute dès le montage avec la valeur initiale
Option deep — surveiller un objet imbriqué
const user = reactive({ name: 'Alice', address: { city: 'Paris' } })
watch(user, (newUser) => {
console.log('user a changé :', newUser)
}, { deep: true }) // sans deep, les changements de user.address.city ne déclenchent rien
reactive(), Vue auto-applique deep: true. Avec un ref d'objet, il faut l'activer manuellement.
Plusieurs sources — tableau
const firstName = ref('Alice')
const lastName = ref('Martin')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`${oldFirst} ${oldLast} → ${newFirst} ${newLast}`)
})
Watcher getter — surveiller une propriété dérivée
const state = reactive({ count: 0 })
watch(() => state.count, (newCount) => {
console.log('count est maintenant', newCount)
})
Stopper un watcher manuellement
const stop = watch(query, (val) => { /* ... */ })
// plus tard :
stop() // le watcher ne se déclenchera plus
3. watchEffect
watchEffect(fn) exécute immédiatement la fonction et la ré-exécute automatiquement chaque fois qu'une dépendance réactive utilisée à l'intérieur change.
Pas besoin de déclarer les sources : Vue les détecte automatiquement.
Comparaison watch vs watchEffect
| watch | watchEffect | |
|---|---|---|
| Sources | Explicites | Auto-détectées |
| Exécution initiale | Non (sauf immediate) | Oui, toujours |
| Accès old/new value | Oui | Non |
| Usage idéal | Réagir à un changement précis | Effets de bord généraux |
Exemple watchEffect
<script setup>
import { ref, watchEffect } from 'vue'
const userId = ref(1)
const userData = ref(null)
watchEffect(async () => {
// userId.value est lu → Vue track cette dépendance
const res = await fetch(`/api/users/${userId.value}`)
userData.value = await res.json()
// Si userId change, watchEffect relance automatiquement
})
</script>
Stopper watchEffect
const stop = watchEffect(() => {
console.log('count:', count.value)
})
// Stopper après 5 secondes
setTimeout(() => stop(), 5000)
Nettoyage avec onCleanup
watchEffect((onCleanup) => {
const controller = new AbortController()
fetch(`/api/search?q=${query.value}`, {
signal: controller.signal
}).then(/* ... */)
onCleanup(() => {
controller.abort() // annule la requête précédente si query change
})
})
watchEffect est idéal pour synchroniser des effets de bord avec plusieurs réactifs. Utilisez watch quand vous avez besoin des valeurs old/new ou d'un contrôle précis sur les sources.
4. nextTick
Vue met à jour le DOM de façon asynchrone : quand on modifie une ref, le DOM n'est pas mis à jour immédiatement.
nextTick() retourne une Promise qui se résout après que Vue a terminé de mettre à jour le DOM.
Pourquoi en avons-nous besoin ?
<script setup>
import { ref } from 'vue'
const show = ref(false)
const inputRef = ref(null)
async function showAndFocus() {
show.value = true
// ERREUR : inputRef.value est null ici, le DOM n'est pas encore mis à jour !
// inputRef.value.focus()
}
</script>
Solution avec nextTick
<script setup>
import { ref, nextTick } from 'vue'
const show = ref(false)
const inputRef = ref(null)
async function showAndFocus() {
show.value = true
await nextTick() // attend que Vue mette à jour le DOM
inputRef.value?.focus() // maintenant l'input est dans le DOM
}
</script>
<template>
<input v-if="show" ref="inputRef" placeholder="focus auto" />
<button @click="showAndFocus">Afficher & focus</button>
</template>
Version callback (ancienne syntaxe)
import { nextTick } from 'vue'
nextTick(() => {
// DOM mis à jour
console.log(document.querySelector('#result').textContent)
})
nextTick est particulièrement utile dans les tests unitaires, et pour interagir avec des bibliothèques DOM tierces après une mise à jour réactive.
5. <KeepAlive>
Par défaut, quand on bascule entre composants avec v-if ou des vues dynamiques, Vue détruit et recrée les composants à chaque fois.
<KeepAlive> met en cache le composant pour conserver son état.
Sans KeepAlive — état perdu
<template>
<button @click="tab = 'A'">Onglet A</button>
<button @click="tab = 'B'">Onglet B</button>
<!-- Sans KeepAlive : chaque switch détruit/recrée le composant -->
<ComponentA v-if="tab === 'A'" />
<ComponentB v-if="tab === 'B'" />
</template>
Avec KeepAlive — état conservé
<template>
<KeepAlive>
<component :is="tab === 'A' ? ComponentA : ComponentB" />
</KeepAlive>
</template>
include / exclude — filtrer les composants mis en cache
<!-- Par nom de composant -->
<KeepAlive include="ComponentA,ComponentB">
<component :is="currentTab" />
</KeepAlive>
<!-- Regex -->
<KeepAlive :include="/^(Tab|Panel)/">
<component :is="currentTab" />
</KeepAlive>
<!-- Exclure certains -->
<KeepAlive exclude="HeavyChart">
<component :is="currentTab" />
</KeepAlive>
max — limiter le nombre de composants en cache
<KeepAlive :max="5">
<component :is="currentTab" />
</KeepAlive>
Hooks spéciaux : onActivated / onDeactivated
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('Composant activé (sorti du cache)')
// Bon moment pour rafraîchir des données
})
onDeactivated(() => {
console.log('Composant mis en cache (pas détruit)')
})
</script>
KeepAlive est parfait pour les onglets, les formulaires multi-étapes ou les listes avec scroll position, où l'utilisateur s'attend à retrouver son état.
6. <Teleport>
<Teleport> permet de rendre le contenu d'un composant dans un autre élément du DOM, en dehors de la hiérarchie normale de l'arbre de composants.
C'est la solution idéale pour les modals, tooltips et notifications qui doivent s'afficher par-dessus tout.
Problème sans Teleport
<!-- Composant profondément imbriqué -->
<div class="card" style="overflow:hidden; position:relative; z-index:1">
<!-- Le modal est piégé dans ce conteneur ! -->
<div class="modal" style="position:fixed; top:0; left:0">
Je suis censé couvrir toute la page...
</div>
</div>
Solution avec Teleport
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<template>
<button @click="showModal = true">Ouvrir Modal</button>
<!-- Ce contenu sera inséré dans document.body, pas ici -->
<Teleport to="body">
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
<div class="modal-box">
<h2>Modal Teleporté</h2>
<p>Je suis rendu dans <body> même si je suis déclaré ici.</p>
<button @click="showModal = false">Fermer</button>
</div>
</div>
</Teleport>
</template>
Cibles possibles
<!-- Dans body -->
<Teleport to="body">...</Teleport>
<!-- Dans un élément avec un id -->
<Teleport to="#app-notifications">...</Teleport>
<!-- Désactiver conditionnellement -->
<Teleport to="body" :disabled="isMobile">...</Teleport>
Plusieurs Teleport vers la même cible
<!-- Ils s'ajoutent dans l'ordre -->
<Teleport to="#notifications"><Toast msg="Succès" /></Teleport>
<Teleport to="#notifications"><Toast msg="Erreur" /></Teleport>
Teleport reste pleinement dans l'arbre de composants Vue : les props, events et provide/inject fonctionnent normalement.