Développement Web

Réactivité Vue 3 : ref, reactive, computed et watch

14 min de lecture
📍 Article principal : Vue 3 et la Composition API : le guide complet. Pour la vue d’ensemble du parcours, commencez par là.

Quand l’écran refuse de bouger

Vous avez un champ de recherche et une liste de notes. L’utilisateur tape « facture », et… rien ne se passe. La liste reste figée. En JavaScript classique, il faudrait écouter l’événement input, retrouver chaque élément du DOM, le masquer ou l’afficher à la main. Vue règle ce problème d’une autre manière : vous déclarez que la liste affichée dépend du texte tapé, et le framework se charge de tout redessiner au bon moment. Ce mécanisme s’appelle la réactivité, et c’est le cœur de Vue.

Dans ce tutoriel, vous apprenez les quatre outils qui composent ce système — ref, reactive, computed et watch — en construisant la recherche en direct et les compteurs de NoteFlux, le gestionnaire de notes qui sert de fil rouge à tout le parcours. À la fin, vous saurez modéliser un état qui réagit, sans manipuler le DOM une seule fois.

Ce que vous allez apprendre

  • Déclarer un état réactif avec ref et comprendre le rôle de .value.
  • Rendre un objet entier réactif avec reactive, et savoir quand préférer l’un ou l’autre.
  • Dériver des valeurs calculées et mises en cache avec computed.
  • Déclencher des effets de bord avec watch et watchEffect.
  • Éviter les trois pièges classiques qui « cassent » la réactivité.

Ce que vous allez construire

Une page NoteFlux qui affiche une liste de notes, un champ de recherche qui filtre cette liste à chaque frappe, et deux compteurs en direct : le nombre de notes affichées et le nombre de favoris. Tout se met à jour instantanément, sans rechargement et sans une seule ligne de manipulation du DOM.

Prérequis

  • Node.js 20 ou plus récent et un éditeur (VS Code conseillé).
  • Une base en JavaScript moderne : const, fonctions fléchées, déstructuration, méthodes de tableau comme filter.
  • Un projet Vue 3 créé avec npm create vue@latest. Niveau débutant accepté.
  • ⏱️ Temps estimé : ~40 minutes.

Test express : si vous savez écrire [1,2,3].filter(n => n > 1) et expliquer ce qu’il renvoie, vous êtes prêt. Sinon, révisez d’abord les méthodes de tableau JavaScript.

Étape 1 — Déclarer un état réactif avec ref

Tout commence par une question : comment stocker une donnée pour que Vue sache la surveiller ? La réponse de base est ref. Cette fonction enveloppe n’importe quelle valeur — un nombre, une chaîne, un tableau, un objet — dans un conteneur réactif. On crée un projet, puis on ouvre src/App.vue et on déclare notre premier état :

<script setup>
import { ref } from 'vue'

const recherche = ref('')
const notes = ref([
  { id: 1, titre: 'Facture client Diop', favori: true },
  { id: 2, titre: 'Idées pour la page d\'accueil', favori: false },
  { id: 3, titre: 'Liste de courses bureau', favori: false }
])
</script>

Deux choses à retenir. D’abord, ref('') ne contient pas directement la chaîne vide : il renvoie un objet dont la valeur réelle est rangée dans une propriété .value. Pour lire ou écrire la donnée dans le script, on passe donc toujours par recherche.value. Ensuite — et c’est la source de confusion numéro un — dans le gabarit, Vue « déballe » automatiquement le ref : on écrit recherche, pas recherche.value. Cette asymétrie déroute au début, puis devient un réflexe.

Affichons maintenant ces données. Vue redessinera la liste dès que notes ou recherche changera :

<template>
  <input v-model="recherche" placeholder="Rechercher une note">
  <ul>
    <li v-for="note in notes" :key="note.id">{{ note.titre }}</li>
  </ul>
</template>

La directive v-model crée une liaison à double sens entre le champ de saisie et recherche : tapez dans l’input, et recherche.value se met à jour tout seul. C’est déjà de la réactivité en action.

Point d’étape — Lancez npm run dev. Vous devez voir les trois notes listées et un champ de recherche. Si la liste est vide, vérifiez que vous avez bien mis :key sur le v-for et que notes est bien un ref contenant un tableau.

Étape 2 — Rendre un objet réactif avec reactive

ref convient à tout, mais quand on a un groupe de valeurs liées — par exemple les réglages d’affichage de NoteFlux —, reactive est parfois plus naturel. reactive prend un objet et en renvoie une version profondément réactive : modifier n’importe quelle propriété, même imbriquée, déclenche les mises à jour.

import { reactive } from 'vue'

const reglages = reactive({
  trier: 'recent',
  afficherFavoris: false
})

// pas de .value : on accède directement aux proprietes
reglages.afficherFavoris = true

La différence pratique avec ref : pas de .value, on lit et on écrit les propriétés directement. En contrepartie, reactive a deux limites importantes. Il ne fonctionne qu’avec des objets, des tableaux et les collections Map/Set — pas avec un nombre ou une chaîne seuls. Et surtout, on ne peut pas remplacer l’objet entier (reglages = {...} casse la réactivité), seulement muter ses propriétés.

Quelle règle simple adopter ? Pour la plupart des cas, utilisez ref par défaut : il marche partout et reste cohérent. Réservez reactive aux objets de configuration locaux que vous ne réassignez jamais. C’est la recommandation que suit la documentation officielle, et elle évite bien des hésitations.

Point d’étape — Ajoutez un bouton <button @click="reglages.afficherFavoris = !reglages.afficherFavoris">Favoris</button> et affichez {{ reglages.afficherFavoris }}. Le texte doit basculer entre true et false à chaque clic.

Étape 3 — Dériver des valeurs avec computed

Voici le moment clé. Notre liste filtrée n’est pas une donnée que l’on stocke : c’est une donnée que l’on calcule à partir de notes et de recherche. Pour ça, on utilise computed. Une valeur calculée se déclare comme une fonction, et Vue la recalcule automatiquement — mais seulement quand l’une de ses dépendances change. Entre deux changements, le résultat est mis en cache, donc le calcul ne tourne pas inutilement à chaque rendu.

import { ref, computed } from 'vue'

const notesFiltrees = computed(() => {
  const terme = recherche.value.trim().toLowerCase()
  let liste = notes.value
  if (reglages.afficherFavoris) liste = liste.filter(n => n.favori)
  if (terme) liste = liste.filter(n => n.titre.toLowerCase().includes(terme))
  return liste
})

const nbAffichees = computed(() => notesFiltrees.value.length)
const nbFavoris = computed(() => notes.value.filter(n => n.favori).length)

Observez la mécanique. notesFiltrees lit recherche.value, reglages.afficherFavoris et notes.value : Vue enregistre ces trois dépendances. Dès que l’une bouge, la valeur est marquée « périmée » et recalculée à la prochaine lecture. nbAffichees dépend lui-même de notesFiltrees : les calculs se chaînent naturellement. On remplace v-for="note in notes" par v-for="note in notesFiltrees" dans le gabarit, on ajoute les compteurs, et la recherche en direct fonctionne :

<p>{{ nbAffichees }} note(s) affichée(s) — {{ nbFavoris }} favori(s)</p>
<ul>
  <li v-for="note in notesFiltrees" :key="note.id">{{ note.titre }}</li>
</ul>

Pourquoi ne pas simplement écrire une méthode qui filtre ? Parce qu’une méthode se rejoue à chaque rendu, même si rien n’a changé, alors qu’un computed ne recalcule que si une dépendance a bougé. Sur une liste de trois notes, la différence est invisible ; sur une liste de mille, elle compte. C’est la règle d’or : une valeur dérivée d’un état réactif est presque toujours un computed.

Point d’étape — Tapez dans le champ. La liste doit se réduire en temps réel et le compteur « notes affichées » suivre. Si rien ne bouge, vérifiez que vous lisez bien recherche.value (avec .value) à l’intérieur du computed.

Étape 4 — Réagir aux changements avec watch

computed sert à produire une valeur. Mais parfois on veut déclencher une action quand une donnée change : enregistrer dans le stockage local, appeler une API, journaliser. C’est le rôle de watch. Il surveille une ou plusieurs sources réactives et exécute une fonction quand elles changent, en fournissant l’ancienne et la nouvelle valeur.

import { watch } from 'vue'

watch(recherche, (nouveau, ancien) => {
  console.log('Recherche : de \"' + ancien + '\" vers \"' + nouveau + '\"')
})

// surveiller plusieurs sources a la fois
watch([recherche, () => reglages.afficherFavoris], () => {
  localStorage.setItem('noteflux-filtre', recherche.value)
})

Le premier argument de watch peut être un ref, une fonction qui renvoie une valeur (utile pour observer une propriété d’un objet reactive), ou un tableau de sources. Notez le () => reglages.afficherFavoris : pour surveiller une propriété d’un objet réactif, on passe une fonction qui la lit, pas la propriété directement.

Il existe une variante plus concise, watchEffect, qui exécute immédiatement une fonction et suit automatiquement toutes les valeurs réactives qu’elle lit — sans déclarer les sources :

import { watchEffect } from 'vue'

watchEffect(() => {
  document.title = 'NoteFlux — ' + nbAffichees.value + ' note(s)'
})

Ici, le titre de l’onglet se met à jour à chaque changement du nombre de notes affichées, parce que la fonction lit nbAffichees.value. watchEffect est parfait pour les effets « toujours synchronisés avec l’état » ; watch reste préférable quand on a besoin de l’ancienne valeur ou d’un contrôle fin sur les sources.

Un mot sur le nettoyage. Si votre effet lance une requête réseau, vous voulez annuler la précédente quand une nouvelle part. Vue 3.5 a introduit onWatcherCleanup pour ça, à appeler à l’intérieur du watcher :

import { watch, onWatcherCleanup } from 'vue'

watch(recherche, () => {
  const controleur = new AbortController()
  fetch('/api/recherche?q=' + recherche.value, { signal: controleur.signal })
  onWatcherCleanup(() => controleur.abort())
})

Point d’étape — Ouvrez la console du navigateur et tapez dans la recherche : vous devez voir les messages « de … vers … ». L’onglet du navigateur doit afficher le nombre de notes. Si le titre ne change pas, vérifiez que watchEffect lit bien nbAffichees.value.

Étape 5 — Les pièges qui cassent la réactivité

Trois erreurs reviennent sans cesse. La première : déstructurer un objet reactive. Écrire const { afficherFavoris } = reglages copie la valeur à l’instant T et rompt le lien réactif — la variable ne se mettra plus jamais à jour. La parade est toRefs, qui transforme chaque propriété en ref indépendant tout en gardant le lien :

import { toRefs } from 'vue'
const { afficherFavoris, trier } = toRefs(reglages)
// afficherFavoris est maintenant un ref relie a reglages.afficherFavoris

La deuxième : oublier .value dans le script. if (recherche) est toujours vrai, car recherche est l’objet enveloppe, jamais vide ; il fallait écrire if (recherche.value). La troisième : perdre la réactivité après un await dans certains contextes — lisez vos valeurs réactives avant la coupure asynchrone, ou structurez l’effet avec watch. Garder ces trois réflexes en tête vous épargnera l’essentiel des bugs de débutant.

Étape 6 — Vérification finale

Tout doit maintenant tenir ensemble. Tapez « facture » : seule la note de Diop reste, le compteur passe à 1, le titre de l’onglet se met à jour. Activez les favoris : la note de Diop, marquée favorite, reste affichée. Videz le champ : les trois notes reviennent. Rechargez la page : la dernière recherche tapée a été conservée dans le stockage local par le watch. Si chacun de ces comportements répond sans le moindre code de manipulation du DOM, vous avez compris la réactivité de Vue.

Pièges fréquents

Symptôme Cause probable Correctif
La valeur affiche [object Object] ou ne change pas Oubli de .value dans le script, ou ajout d’un .value en trop dans le gabarit .value dans le script uniquement ; jamais dans le gabarit
Une variable déstructurée reste figée Déstructuration directe d’un objet reactive Passer par toRefs avant de déstructurer
Le computed ne se recalcule jamais Il ne lit aucune source réactive (valeurs lues hors du getter) Lire les dépendances à l’intérieur de la fonction du computed
Remplacer un objet reactive n’a aucun effet Réassignation complète (reglages = {...}) Muter les propriétés, ou utiliser un ref pour l’objet entier
Le watch sur une propriété ne se déclenche pas Source passée en valeur au lieu d’un getter Passer () => objet.prop comme source

Réalités du terrain

La réactivité a un coût mémoire, et Vue 3.5 l’a justement réduit d’environ 56 % par rapport aux versions précédentes — un gain qui se ressent sur les appareils modestes. Deux habitudes prolongent ce bénéfice. D’abord, ne rendez réactif que ce qui doit l’être : une grosse liste figée que vous n’affichez qu’en lecture peut rester un simple tableau, sans ref, ou utiliser shallowRef pour éviter de rendre réactive chaque ligne en profondeur. Ensuite, privilégiez computed aux méthodes pour tout calcul répété : la mise en cache évite des recalculs qui, multipliés, font ramer une interface sur un téléphone d’entrée de gamme. Ces réflexes ne coûtent rien à l’écriture et se voient immédiatement à l’usage sur du matériel limité.

Récapitulatif

Vous venez de construire la recherche en direct de NoteFlux et ses compteurs, sans toucher au DOM. Vous savez désormais déclarer un état avec ref (et son .value), grouper des valeurs avec reactive, dériver des données mises en cache avec computed, et déclencher des effets avec watch et watchEffect. Surtout, vous connaissez les trois pièges — déstructuration, .value oublié, coupure asynchrone — qui causent l’essentiel des bugs de réactivité. C’est le socle sur lequel tout le reste de Vue repose.

Aide-mémoire

Outil Rôle
ref(v) État réactif pour toute valeur ; accès par .value dans le script
reactive(obj) Objet/tableau profondément réactif ; accès direct, pas de réassignation
computed(getter) Valeur dérivée, mise en cache, recalculée à la demande
watch(src, cb) Réagit au changement d’une source ; donne ancien/nouveau
watchEffect(fn) Exécute et suit automatiquement les dépendances lues
toRefs(obj) Déstructurer un reactive sans perdre la réactivité
onWatcherCleanup(fn) Nettoyer un effet (annuler une requête) — Vue 3.5+

À vous de jouer

Ajoutez à NoteFlux un computed nommé resume qui renvoie une phrase du type « 2 notes sur 3, dont 1 favori », et faites-la s’afficher sous la liste. Bonus : ajoutez un watch qui, quand la recherche ne renvoie aucun résultat, journalise le terme cherché (utile pour savoir ce que les utilisateurs ne trouvent pas).

Voir une solution
const resume = computed(() =>
  nbAffichees.value + ' note(s) sur ' + notes.value.length + ', dont ' + nbFavoris.value + ' favori(s)'
)

watch(notesFiltrees, (liste) => {
  if (liste.length === 0 && recherche.value) {
    console.log('Aucun résultat pour : ' + recherche.value)
  }
})

Tutoriels frères

Pour aller plus loin

FAQ

ref ou reactive : que choisir ?
ref par défaut, car il fonctionne avec tout type de valeur et reste cohérent. reactive pour un objet de configuration local que l’on ne réassigne jamais. En cas de doute, ref.

Pourquoi .value dans le script mais pas dans le gabarit ?
Parce que le compilateur de gabarit « déballe » automatiquement les refs de premier niveau pour vous épargner le .value répétitif à l’affichage. Dans le script JavaScript, ce déballage n’existe pas : il faut .value.

computed peut-il être modifiable ?
Oui. En passant un objet { get, set } à computed, on crée une valeur calculée que l’on peut aussi écrire. C’est utile pour adapter un v-model à une donnée transformée, mais à réserver aux cas réels.

Faut-il toujours nettoyer ses watchers ?
Les watchers créés dans <script setup> sont automatiquement arrêtés quand le composant est démonté. Le nettoyage manuel via onWatcherCleanup ne concerne que les effets internes comme les requêtes réseau à annuler.

Partager