Comprendre watch() dans Vue.js
Dans notre projet Vue, il arrive que nous ayons besoin de réagir à un changement sans vouloir afficher de nouvelle valeur, ni déclencher une action manuelle. Juste écouter une évolution de l’état, et agir en conséquence. C’est exactement le rôle de watch() : un outil qui observe silencieusement une donnée, et qui s’active dès qu’elle bouge.
Contrairement à computed, qui calcule une valeur destinée à être affichée, ou à methods, qui attend un déclencheur explicite, watch() se place entre les deux. Il ne produit rien en soi, mais il déclenche une action dès que l’état évolue. Dans notre application vidéo, par exemple, c’est ce qui nous permet de réinitialiser un lecteur, de relancer une animation, ou d’adapter un affichage sans intervention directe de l’utilisateur.
Encore faut-il comprendre comment cette fonction fonctionne : ce qu’elle peut surveiller, quand elle s’exécute, et à quoi elle sert vraiment. C’est ce que nous allons explorer ensemble dans ce nouvel article, en suivant notre fil rouge habituel — celui d’une interface vidéo interactive, simple mais riche en cas concrets.
À quoi sert vraiment watch() ?
La fonction watch() n’est pas là pour afficher une donnée, ni pour calculer une nouvelle valeur. Elle sert à observer une propriété existante — une donnée, une prop, voire une valeur calculée — et à exécuter une action dès que cette propriété change. Ce n’est pas une fonction que nous appelons nous-mêmes : c’est Vue qui s’en charge, de façon automatique et réactive.
L’intérêt de watch() apparaît dès que nous avons besoin de déclencher un effet de bord en réponse à une modification. Cela peut être une requête à un serveur, une réinitialisation d’interface, une sauvegarde locale, un changement d’onglet ou de style, ou encore une animation. Des actions qui n’ont pas vocation à être visibles par elles-mêmes, mais qui doivent se produire au bon moment.
Dans notre application vidéo, par exemple, chaque fois que l’utilisateur choisit une nouvelle vidéo, nous voulons mettre à jour la source du lecteur, réinitialiser le mode plein écran ou masquer les contrôles précédents. Aucun affichage ne dépend directement de cette action, et aucune méthode ne devrait être déclenchée manuellement. C’est justement pour ce type de scénario que watch() s’impose naturellement.
Il suffit de lui indiquer quelle donnée surveiller, et Vue exécutera la fonction associée dès qu’un changement est détecté. Ni plus, ni moins.
Exemple concret : changement de vidéo
Imaginons une interface où l’utilisateur peut cliquer sur une miniature pour lancer une vidéo. Ce clic modifie une propriété appelée selectedVideoId, que nous avons définie dans data(). Ce que nous voulons, c’est que ce simple changement déclenche automatiquement une action : mettre à jour la source du lecteur, sans intervention supplémentaire.
Plutôt que de déclencher une méthode à chaque clic, nous allons laisser watch() faire le travail. Dès que selectedVideoId change, Vue exécute la fonction associée et met à jour notre propriété selectedVideo. Voici à quoi cela peut ressembler :
export default {
data() {
return {
selectedVideoId: null,
selectedVideo: null,
};
},
watch: {
selectedVideoId(newId) {
this.selectedVideo = `https://www.youtube.com/embed/${newId}?autoplay=1&rel=0`;
// On peut aussi désactiver la lightbox ou réinitialiser un minuteur ici
}
}
}Dans cet exemple, tout repose sur une seule chose : la mise à jour de selectedVideoId. C’est cette variable que le reste de l’interface (iframe, modale, contrôles) utilise comme déclencheur implicite. Nous ne faisons appel à aucune méthode depuis le HTML, ni ne passons par une logique manuelle : Vue observe et réagit.
Ce type de configuration est particulièrement utile quand une action dépend d’un changement d’état, mais ne produit pas de nouvelle valeur à afficher. Le watch() devient alors un petit relais discret, qui connecte les intentions de l’utilisateur aux comportements internes de l’interface.
Quand préférer watch() à computed ou methods ?
Quand on découvre watch(), une question revient souvent : pourquoi l’utiliser, alors qu’on a déjà computed pour les valeurs réactives, et methods pour les actions ? En réalité, tout dépend du type de réaction que nous attendons, et surtout du moment où cette réaction doit se produire.
Un computed est parfait lorsqu’on souhaite afficher une valeur calculée à partir d’autres, comme un résultat intermédiaire ou une liste filtrée. Dans notre application vidéo, par exemple, nous utilisons une propriété computed pour générer automatiquement une liste filteredVideos, qui dépend de plusieurs critères : mot-clé recherché, genre sélectionné, groupe actif. Dès qu’un de ces filtres change, Vue recalcule la liste à afficher — mais uniquement si c’est nécessaire. Ce mécanisme permet d’avoir une interface fluide et réactive, sans recalculs superflus. C’est une logique de lecture optimisée : on transforme des données en résultat affichable, sans déclencher d’effet annexe.
Une method, de son côté, est déclenchée manuellement. On l’appelle à la suite d’un clic, d’un événement clavier, ou d’une interaction utilisateur explicite. Dans notre interface vidéo, c’est le cas lorsqu’on utilise resetFilters() pour effacer tous les critères de tri, ou toggleForm() pour afficher ou masquer un panneau. Ces méthodes ne dépendent pas de l’évolution d’une donnée, mais d’une action directe. Elles sont idéales pour les gestes ponctuels, où c’est à nous — et non à Vue — de décider du moment où le code s’exécute.
Avec watch(), nous entrons dans une logique différente. L’objectif n’est plus d’afficher une valeur ni de répondre à un geste explicite, mais de réagir automatiquement à un changement d’état. Cela peut être l’occasion de déclencher une requête vers un serveur, de modifier un comportement interne, de réinitialiser un minuteur, ou d’appliquer un effet visuel. Ce que nous surveillons n’a pas besoin d’être visible à l’écran : il suffit que cela évolue pour que Vue exécute la fonction correspondante.
Dans notre cas, imaginons que l’utilisateur sélectionne une nouvelle vidéo. Ce simple changement de sélection ne nécessite pas un affichage supplémentaire, ni un bouton à cliquer. Ce que nous voulons, c’est enchaîner une série d’actions en coulisse : mettre à jour l’URL du lecteur, masquer la lightbox précédente, interrompre une lecture en cours, ou même ajuster un état d’interface. Aucun computed ne pourrait faire cela — il ne renvoie qu’une valeur. Aucune method ne devrait le déclencher — il n’y a pas d’événement manuel. C’est exactement dans ce type de scénario que watch() trouve toute sa place.
Les pièges à éviter avec watch()
Si watch() est un outil puissant, il peut aussi provoquer des effets inattendus quand on ne maîtrise pas bien la façon dont Vue suit les données. Dans les cas simples — comme un ID ou une chaîne de caractères — tout fonctionne comme prévu. Mais dès qu’on touche à des objets plus complexes, ou à des structures imbriquées, quelques subtilités apparaissent.
Le premier piège, c’est de modifier un tableau ou un objet sans changer sa référence. Dans notre application vidéo, imaginons que nous ajoutions une vidéo dans le tableau videos avec un simple this.videos.push(...). Vue ne détectera pas forcément ce changement, car la référence du tableau n’a pas bougé. Le watch() associé à videos ne s’exécutera donc pas. Pour garantir une réaction, il faut modifier la structure elle-même, par exemple en remplaçant entièrement le tableau :
this.videos = [...this.videos, newVideo];Deuxième point d’attention : les propriétés imbriquées. Si nous voulons surveiller un objet comme selectedVideo, qui contient plusieurs champs (id, label, duration, etc.), un watch() classique sur selectedVideo ne suffira pas à détecter les changements internes. Vue ne voit que la référence de l’objet, pas ce qui change à l’intérieur.
Dans ce cas, il faut activer l’option deep: true :
watch: {
selectedVideo: {
handler(newValue) {
// réagir à un changement interne
},
deep: true
}
}Enfin, il arrive qu’on veuille exécuter un watch() dès le montage, sans attendre un premier changement. C’est utile par exemple pour envoyer une requête dès qu’une valeur est définie. Là encore, Vue ne le fait pas par défaut. Il faut ajouter immediate: true dans la configuration :
watch: {
searchQuery: {
handler(query) {
this.fetchSuggestions(query);
},
immediate: true
}
}Ces options ne sont pas à utiliser systématiquement, mais elles sont précieuses dans les cas où le comportement “normal” de watch() ne suffit plus. Comprendre comment Vue observe — ou n’observe pas — certains changements, c’est éviter de perdre du temps à déboguer un code qui « devrait marcher ».
Vue.js vous écoute… mais pas toujours : pourquoi activer deep ou immediate
Lorsqu’on commence à utiliser watch() dans Vue.js, les cas simples fonctionnent très bien. Mais dès qu’on touche à des objets plus complexes, ou qu’on veut déclencher une action immédiatement sans attendre un changement, deux options deviennent vite indispensables : deep: true et immediate: true. Leur nom peut sembler technique, mais leur usage est simple, et souvent décisif pour éviter des bugs ou des effets de bord.
Premier cas : surveiller un objet en profondeur (deep)
Imaginons que nous suivions une propriété selectedVideo contenant plusieurs champs : id, title, duration, etc. Si l’on modifie seulement selectedVideo.title, Vue ne considérera pas que selectedVideo a changé… sauf si la référence elle-même est modifiée. Autrement dit, les modifications internes passent inaperçues.
Pour forcer une surveillance profonde, il suffit d’ajouter deep: true dans la déclaration du watcher :
watch: {
selectedVideo: {
handler(newValue) {
// On peut ici réagir à n'importe quel changement interne
console.log('Changement détecté dans selectedVideo :', newValue);
},
deep: true
}
}Cela dit, cette approche a un coût : Vue doit inspecter les propriétés internes à chaque cycle. On l’utilise donc avec précaution, uniquement si l’on sait que les sous-champs peuvent bouger sans que la référence change.
Deuxième cas : déclencher l’action dès le départ (immediate)
Autre situation fréquente : on veut exécuter une action tout de suite, sans attendre qu’une valeur change. C’est utile par exemple pour lancer une requête dès que le composant est monté, ou pour initialiser une liste d’éléments dès que la page charge.
watch: {
searchQuery: {
handler(query) {
this.fetchSuggestions(query);
},
immediate: true
}
}Ici, fetchSuggestions() sera appelée dès que le composant est prêt, même si searchQuery n’a pas encore changé. On évite ainsi un écran vide, ou un délai inutile à l’affichage.
Ces deux options, deep et immediate, ne sont pas indispensables dans tous les cas. Mais les connaître permet d’aller plus loin avec watch(), sans tomber dans des pièges de réactivité difficiles à déboguer. Et surtout, elles aident à mieux comprendre ce que Vue observe… et ce qu’il ignore.
Et du côté de watchEffect() ?
Dans les exemples précédents, nous avons utilisé watch() dans sa version classique, intégrée à l’API Options de Vue. C’est une approche structurée : on déclare une donnée à observer, et une fonction à exécuter en réponse. Mais Vue propose aussi une autre forme de surveillance, plus souple et plus automatique : la fonction watchEffect(), utilisée dans le cadre de la Composition API.
La différence majeure, c’est que watchEffect() n’a pas besoin de savoir à l’avance ce qu’il doit observer. Au lieu de désigner une propriété comme dans watch('x', callback), on lui donne simplement une fonction. Vue l’exécute une première fois immédiatement, puis repère toutes les données réactives accédées pendant cette exécution. Ce sont ces accès qui deviennent les dépendances du watcher.
Prenons un exemple proche de notre application vidéo :
import { ref, watchEffect } from 'vue';
const videoId = ref(null);
const videoSrc = ref(null);
watchEffect(() => {
videoSrc.value = `https://www.youtube.com/embed/${videoId.value}`;
});Ici, videoId.value est lue au moment de l’exécution. Vue en déduit qu’elle doit observer cette propriété. Si videoId.value change plus tard, la fonction est relancée, et videoSrc est mis à jour. Pas besoin de déclarer ce lien à la main : Vue le comprend par l’usage.
Dans un projet structuré avec la Composition API, watchEffect() peut simplifier certaines logiques réactives : affichage conditionnel, réglages dynamiques, ajustement d’interface. Il est idéal pour des effets de bord légers, automatiques et continus, sans avoir à passer par un computed ou un watch() ciblé.
Mais attention : cette syntaxe appartient à une autre organisation du code. Elle suppose qu’on utilise ref(), reactive(), et des blocs setup() plutôt que data() ou methods. Ce n’est donc pas un simple raccourci, mais une autre façon d’écrire une application Vue.
Si vous débutez avec la Composition API ou que vous partez sur un projet neuf, cela mérite d’être exploré. Sinon, l’API Options classique et son watch() restent parfaitement adaptés, surtout dans des interfaces déjà bien en place comme notre application vidéo.
Affiner le déclenchement avec watchPostEffect() et watchSyncEffect()
Vue ne se contente pas de déclencher des réactions dès qu’une donnée change. Elle nous permet aussi d’affiner le moment exact où ces réactions doivent se produire dans le cycle de rendu. C’est tout l’intérêt de watchPostEffect() et watchSyncEffect(), deux fonctions qui ressemblent beaucoup à watchEffect(), mais qui se comportent légèrement différemment — et ce détail peut compter.
Prenons un cas concret : dans une interface vidéo, imaginons qu’on souhaite déclencher une animation juste après que les nouvelles données aient été rendues à l’écran. Si on utilise watchEffect(), la fonction risque d’être appelée avant que le DOM soit mis à jour. Résultat : on anime quelque chose qui n’est pas encore visible.
C’est là qu’intervient watchPostEffect(). Ce watcher s’exécute après le rendu, une fois que les modifications sont visibles à l’écran. C’est idéal pour les effets de transition, les ajustements CSS, ou les opérations qui dépendent du rendu visuel final. On garde la simplicité de watchEffect(), mais avec un meilleur timing.
Inversement, si l’on souhaite une exécution synchrone, dès que la moindre dépendance change, sans attendre le prochain cycle de rendu, watchSyncEffect() entre en jeu. C’est particulièrement utile lorsqu’un élément dépendant doit être prêt avant que le DOM ne se mette à jour. Par exemple, si un champ de formulaire dépend d’un identifiant externe pour générer dynamiquement ses options, et que ce champ est initialisé dans un composant tiers, toute latence peut le désynchroniser. Avec watchSyncEffect(), on injecte les bonnes valeurs avant même que le rendu ne démarre, ce qui évite un clignotement, une erreur d’état, ou un recalcul visuel inutile.
Dans la pratique, voici comment les trois se différencient :
watchEffect()→ exécution dès qu’une dépendance change, pendant le rendu.watchPostEffect()→ exécution après le rendu, une fois le DOM mis à jour.watchSyncEffect()→ exécution immédiate, avant tout autre traitement.
watchEffect() → Pendant le rendu, quand une dépendance change
import { ref, watchEffect } from 'vue';
const userId = ref(1);
const userName = ref('');
watchEffect(() => {
// Exécution dès que userId change (détecté automatiquement)
fetch(`/api/users/${userId.value}`)
.then(res => res.json())
.then(data => {
userName.value = data.name;
});
});Ici, userId est lu dans la fonction. Vue déclenche watchEffect() à chaque changement, y compris au montage, pour maintenir userName à jour. Utile pour suivre une dépendance réactive sans la déclarer.
watchPostEffect() → Après le rendu du DOM
import { ref, watchPostEffect } from 'vue';
const darkMode = ref(false);
watchPostEffect(() => {
// Ce code s’exécute après que le DOM a été mis à jour
document.body.classList.toggle('dark', darkMode.value);
});Ce cas est adapté aux effets visuels ou DOM, qu’on ne veut exécuter qu’après la mise à jour de l’interface. Ici, on ne manipule pas de données Vue, mais le DOM global (ajout d’une classe CSS).
watchSyncEffect() → Avant tout, même avant le rendu
import { ref, watchSyncEffect } from 'vue';
const selectedPlan = ref('free');
const formSchema = ref({});
watchSyncEffect(() => {
// Ce code s’exécute immédiatement avant tout rendu
formSchema.value = selectedPlan.value === 'pro'
? { fields: ['company', 'vat'] }
: { fields: ['name', 'email'] };
});L’intérêt ici est de préparer des données cruciales (structure du formulaire) avant que les composants ne tentent de les lire. Cela évite un rendu partiel ou un affichage incohérent lors d’un changement d’abonnement.
Chacun a sa place
Et tant que les effets restent légers et cohérents, Vue vous laisse libre de choisir le moment le plus pertinent pour les déclencher. C’est une souplesse précieuse, à condition d’en faire un usage ciblé.
Et quand plus rien ne change ? Surveiller l’inactivité dans Vue.js
Un watch() classique sert à réagir quand une donnée change. Mais il arrive que ce soit l’absence de changement qui nous intéresse. Par exemple, pour afficher un message d’inactivité, désactiver un bouton, sauvegarder automatiquement après un délai, ou encore fermer une modale restée ouverte trop longtemps.
La stratégie est alors de redémarrer un minuteur à chaque modification, et d’agir si ce minuteur arrive à son terme sans être relancé. Cela peut se faire très simplement avec un watch() sur la donnée à observer.
Imaginons que notre application possède une valeur searchQuery modifiée à chaque frappe dans un champ de recherche. On souhaite afficher une suggestion seulement si l’utilisateur arrête de taper pendant 2 secondes.
watch(() => searchQuery.value, (newQuery) => {
clearTimeout(timeout.value);
timeout.value = setTimeout(() => {
showSuggestions(newQuery);
}, 2000);
});À chaque modification de searchQuery, on annule le minuteur précédent (clearTimeout), puis on en crée un nouveau (setTimeout). Si l’utilisateur reste inactif pendant 2 secondes, la fonction est appelée.
Cette logique s’applique à bien d’autres cas : déconnexion automatique, animation après une pause, désactivation d’un bouton. Et bien sûr, elle peut être encapsulée dans une méthode dédiée ou un composable si vous utilisez la Composition API.
Mise en garde sur les excès de watch()
Lorsqu’on découvre watch(), il peut être tentant de s’en servir un peu partout. Après tout, c’est simple à mettre en place, ça réagit vite, et ça donne l’impression que tout est bien connecté. Mais comme souvent, ce qui fonctionne trop facilement peut aussi nuire à la lisibilité et à la stabilité du code.
Le premier piège, c’est de multiplier les watch() là où un computed suffirait. Si vous cherchez à obtenir une valeur dérivée, ou à filtrer des résultats selon des critères, un computed est bien plus adapté. Il est plus clair, plus optimisé, et n’introduit aucun effet de bord. Utiliser un watch() dans ce cas reviendrait à écrire manuellement ce que Vue sait déjà faire automatiquement.
Deuxième excès fréquent : déclencher des effets complexes ou en cascade dans plusieurs watch() séparés. Le risque, c’est de perdre le fil : une modification provoque une autre, qui en relance une troisième, sans qu’on sache vraiment d’où vient le déclencheur initial. Cela peut rendre le débogage pénible, surtout si l’interface commence à prendre de l’ampleur.
Enfin, à force de “watcher” tout ce qui bouge, on finit parfois par surveiller des données qui ne changent presque jamais, ou qu’on pourrait simplement gérer autrement. Un bon indicateur : si votre watch() ne sert qu’à affecter une autre donnée, ou qu’il ne s’exécute jamais en pratique, c’est peut-être un morceau de code en trop.
Dans notre application vidéo, nous avons vu que watch() est parfaitement adapté pour déclencher un changement de source ou une transition d’état. Mais au-delà de ces cas précis, il vaut mieux garder une vision claire du rôle de chaque bloc : les valeurs dans data(), les transformations dans computed, les actions manuelles dans methods, et les déclenchements conditionnels dans watch() — mais uniquement quand c’est justifié.
Conclusion
Dans une interface dynamique, tout ne se résume pas à ce qui est visible à l’écran. Certaines actions doivent se produire en coulisse, au bon moment, sans intervention de l’utilisateur. C’est exactement ce que permet watch() : un moyen de réagir proprement à l’évolution des données, sans déclencher manuellement quoi que ce soit, et sans altérer la logique d’affichage.
Dans notre application vidéo, nous l’avons utilisé pour surveiller la sélection d’une vidéo, mettre à jour la source du lecteur, ou réinitialiser des états. Ce sont des cas typiques, où ni computed ni methods ne sont adaptés, car il ne s’agit ni d’une valeur à afficher, ni d’une action déclenchée à la main.
Mais comme tout outil réactif, watch() demande un peu de discipline : savoir quand il est vraiment nécessaire, éviter les effets secondaires non maîtrisés, et garder le code lisible. Une surveillance efficace, oui — mais toujours au service de la clarté.
