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

Ordre complet : setup() → onBeforeMount → onMounted → (réactivité) → onBeforeUpdate → onUpdated → onBeforeUnmount → onUnmounted
HookMomentUsage typique
onBeforeMountAvant insertion dans le DOMPréparer des données
onMountedAprès insertion dans le DOMFetch, refs DOM, setInterval
onBeforeUpdateAvant le re-rendu réactifSauvegarder état du DOM
onUpdatedAprès le re-renduAccéder au DOM mis à jour
onBeforeUnmountAvant destructionNettoyages préparatoires
onUnmountedAprès destructionclearInterval, 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>
Attention : Ne jamais oublier 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
Quand on surveille un 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

watchwatchEffect
SourcesExplicitesAuto-détectées
Exécution initialeNon (sauf immediate)Oui, toujours
Accès old/new valueOuiNon
Usage idéalRéagir à un changement précisEffets 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>
Malgré le déplacement dans le DOM, Teleport reste pleinement dans l'arbre de composants Vue : les props, events et provide/inject fonctionnent normalement.