frontend / vuejs
Computed properties & watchers
Explication
Ce que vous allez apprendre
- Comprendre pourquoi
computedmet en cache son résultat contrairement à une méthode classique - Créer une valeur dérivée à la fois lisible ET modifiable avec un getter/setter
- Distinguer
watch(contrôle explicite) dewatchEffect(suivi automatique des dépendances) - Observer plusieurs sources à la fois, ou en profondeur sur un objet imbriqué
- Éviter le piège classique d'un effet de bord asynchrone placé dans un
computed
Dans quel contexte ?
Une page e-commerce affiche un prix HT et doit calculer et afficher en permanence le prix TTC correspondant, recalculé automatiquement si le taux de TVA change. Ce calcul dérivé, qui ne devrait JAMAIS être recalculé manuellement à chaque endroit où il est affiché, est le cas d'usage exact pour lequel computed a été conçu.
D'abord, une valeur dérivée qui se souvient de son dernier calcul
computed(() => prixHT.value * (1 + tauxTva.value)) ne recalcule sa valeur QUE si l'une de ses dépendances (prixHT ou tauxTva) change réellement. Entre deux changements, la valeur déjà calculée est simplement réutilisée depuis le cache — contrairement à une méthode classique appelée dans le template, qui se réexécuterait à chaque rendu, même si rien n'a changé.
Cette mise en cache automatique est la différence fondamentale entre une computed et une simple fonction : les deux "dérivent" une valeur, mais seule la première évite les recalculs inutiles.
Prérequis
Il faut être à l'aise avec ref() (leçon 3) : une computed lit et suit des refs pour savoir quand se recalculer.
Ensuite, une computed peut aussi accepter d'être modifiée
Une computed avec seulement une fonction est en lecture seule. En lui fournissant un objet { get() {...}, set(nouvelleValeur) {...} }, on peut écrire dans une valeur dérivée — le setter décide alors comment répartir cette écriture sur les valeurs sources réelles, comme diviser un "nom complet" en prénom et nom.
Il reste un besoin que computed ne couvre pas : réagir avec un effet de bord
computed doit rester une fonction pure, sans effet de bord (jamais d'appel réseau, de mutation externe). Pour réagir à un changement en déclenchant une ACTION (appeler une API, logger, mettre à jour le titre de la page), il faut watch ou watchEffect.
| Outil | Suit ses dépendances | S'exécute immédiatement | Contrôle fin (ancienne/nouvelle valeur) |
|---|---|---|---|
computed | Automatiquement | À la première lecture | Non applicable (dérivation pure) |
watch(source, cb) | Sur une source explicite | Non par défaut (immediate: false) | Oui |
watchEffect(fn) | Automatiquement, tout ce qui est lu | Oui, dès la création | Non |
Piège dangereux
Placer un appel fetch() directement dans une computed est un anti-pattern qui casse le contrat de pureté attendu par Vue : une computed peut être évaluée plusieurs fois sans qu'on le contrôle, déclenchant potentiellement des appels réseau en double ou à des moments imprévisibles. Toujours séparer la source réactive (déclenche l'effet) du résultat asynchrone (une ref séparée mise à jour par watch/watchEffect).
Maintenant, gérer une requête asynchrone qui peut arriver dans le désordre
Un watchEffect async qui déclenche une requête réseau doit vérifier, une fois la réponse reçue, que la source n'a pas déjà changé entre-temps (if (idCourant === id.value)). Sans cette vérification, une réponse arrivée en retard pour un ancien identifiant pourrait écraser une réponse plus récente et correcte — une "race condition" classique en JavaScript asynchrone.
Astuce
watch(filtre, cb, { deep: true }) observe les changements à l'intérieur d'un objet imbriqué, mais ce suivi profond a un coût de performance réel sur de grosses structures : à réserver aux cas où c'est vraiment nécessaire, plutôt que de l'appliquer par réflexe partout.
Maintenant que les valeurs dérivées et les effets réactifs sont clairs, la prochaine leçon explore le cycle de vie complet d'un composant : les moments précis où il est créé, monté, mis à jour et détruit.
Commandes & code
Computed properties & watchers
computed dérive une valeur de façon mise en cache ; watch/watchEffect réagissent à un changement pour produire un effet de bord.
import { ref, computed } from 'vue'
const prixHT = ref(100)
const tauxTva = ref(0.2)
// computed en lecture seule : recalculé UNIQUEMENT si ses dépendances (prixHT, tauxTva) changent,
// puis mis en cache jusqu'au prochain changement -> contrairement à une méthode, pas de recalcul à chaque rendu.
const prixTTC = computed(() => prixHT.value * (1 + tauxTva.value))
console.log(prixTTC.value) // 120
prixHT.value = 200
console.log(prixTTC.value) // 240, recalculé automatiquementimport { ref, computed } from 'vue'
const prenom = ref('Ada')
const nom = ref('Lovelace')
// computed avec getter ET setter : permet d'écrire dans une valeur dérivée
const nomComplet = computed({
get() {
return `${prenom.value} ${nom.value}`
},
set(nouvelleValeur) {
const [p, ...reste] = nouvelleValeur.split(' ')
prenom.value = p
nom.value = reste.join(' ')
},
})
nomComplet.value = 'Grace Hopper'
console.log(prenom.value, nom.value) // 'Grace' 'Hopper'import { ref, watch, watchEffect } from 'vue'
const recherche = ref('')
const resultats = ref([])
// watch : réagit à UNE source précise, accès explicite à l'ancienne/nouvelle valeur,
// ne s'exécute PAS immédiatement par défaut.
watch(recherche, async (nouvelleValeur, ancienneValeur) => {
if (!nouvelleValeur.trim()) {
resultats.value = []
return
}
const reponse = await fetch(`/api/recherche?q=${encodeURIComponent(nouvelleValeur)}`)
resultats.value = await reponse.json()
}, {
debounce: undefined, // Vue n'a pas de debounce natif : à implémenter soi-même (voir lesson composables)
immediate: false, // true = exécute aussi au montage
})
// watch sur plusieurs sources à la fois
const largeur = ref(0)
const hauteur = ref(0)
watch([largeur, hauteur], ([l, h], [ancienL, ancienH]) => {
console.log(`aire: ${l * h}`)
})
// watch profond sur un objet imbriqué (coûteux : parcourt toute la structure)
const filtre = ref({ categorie: 'tous', prixMax: 1000 })
watch(filtre, (nv) => console.log('filtre changé', nv), { deep: true })
// watchEffect : suit AUTOMATIQUEMENT toutes les refs/reactive lues dans son corps,
// s'exécute immédiatement une première fois (pas besoin de `immediate: true`).
watchEffect(() => {
console.log(`recherche actuelle: "${recherche.value}", ${resultats.value.length} résultat(s)`)
})
// Arrêter un watcher manuellement (utile hors du cycle de vie normal d'un composant)
const arreter = watchEffect(() => console.log(recherche.value))
arreter() // stoppe l'observation// Piège classique : computed() ne doit JAMAIS avoir d'effet de bord (ex: appel réseau, mutation externe)
// -> pour un calcul asynchrone dérivé, utiliser watch/watchEffect + une ref séparée pour le résultat.
// Mauvais : effet de bord dans un computed
// const donnees = computed(() => { fetch('/api/x').then(...) }) // NE PAS FAIRE
// Bon : séparer la source réactive du résultat asynchrone
import { ref, watchEffect } from 'vue'
const id = ref(1)
const utilisateur = ref(null)
watchEffect(async () => {
const idCourant = id.value
const reponse = await fetch(`/api/utilisateurs/${idCourant}`)
// vérifie que id n'a pas changé pendant l'attente (évite une "race condition")
if (idCourant === id.value) {
utilisateur.value = await reponse.json()
}
})Résumé
computed: dérivation pure, mise en cache, jamais d'effet de bord ni d'appel async direct.watch(source, cb): contrôle fin (ancienne/nouvelle valeur,deep,immediate), déclenché explicitement.watchEffect(fn): suit automatiquement ses dépendances, s'exécute immédiatement.- Pour du dérivé asynchrone, combiner une
refde résultat avecwatch/watchEffect, jamais uncomputedasync.
Exercices pratiques
Mission : stopper des appels réseau en boucle
Objectif : Diagnostiquer un appel réseau placé dans un computed, puis le remplacer par un watch sûr face aux réponses tardives.
Contexte
Un composant FicheUtilisateur reçoit un idUtilisateur réactif et affiche ses informations via const utilisateur = computed(() => { fetch(/api/utilisateurs/${idUtilisateur.value}).then(r => r.json()).then(d => resultat = d); return resultat }). En production, les logs réseau montrent des dizaines d'appels à la même URL en quelques secondes, et parfois la fiche affiche les infos d'un AUTRE utilisateur que celui actuellement sélectionné.