La réactivité
Introduction
L'un des points forts de Vue.js est son système réactif. Il permet de lier des données JavaScript à l'affichage HTML de manière simple et automatique. Concrètement, lorsque la valeur d'une donnée change, Vue met à jour l'affichage sans qu'il soit nécessaire d'écrire manuellement du code pour modifier le DOM.
Cette synchronisation entre les données et l'interface est au cœur du fonctionnement de Vue. Elle repose sur deux éléments :
- Une donnée réactive définie dans la partie JavaScript.
- Une liaison entre cette donnée et l'affichage HTML.
Créer une donnée réactive avec ref()
Jusqu'ici, nous avons affiché des données statiques avec les moustaches ({{ }}) et la directive v-html. Autrement dit, si ces données étaient modifiées après le démarrage du programme dans le bloc <script>, leur affichage ne se mettait pas à jour automatiquement dans le <template>.
Pour qu'une donnée soit réactive, elle doit être créée avec une fonction spéciale comme ref(). Ainsi, toute modification de sa valeur dans la partie <script> entraîne automatiquement la mise à jour des éléments du <template> qui l'affichent.
Dans l'exemple suivant, nous utilisons l'évènement click sur les deux balises <button> pour changer la valeur d'une donnée réactive (statutCommande) et observer la mise à jour instantanée à l'écran.
<script setup>
// Importer la fonction ref() pour créer des données réactives.
import { ref } from 'vue';
// Donnée réactive représentant le statut d'une commande.
const statutCommande = ref('En préparation');
// Fonctions qui modifient l'état via la portée du composant.
const expedierCommande = () => { statutCommande.value = 'Expédiée'; };
const annulerCommande = () => { statutCommande.value = 'Annulée'; };
</script>
<template>
<p>Statut de la commande : {{ statutCommande }}</p>
<button type="button" @click="expedierCommande">Expédier</button>
<button type="button" @click="annulerCommande">Annuler</button>
</template>
Grâce à la réactivité de Vue, l'interface se met à jour immédiatement. Ainsi {{ statutCommande }} reflètent la nouvelle valeur, sans nécessité de manipuler le DOM comme on le ferait en Javascript vanilla.
Ce comportement est rendu possible par la fonction ref(), qui enveloppe la valeur dans un objet réactif doté d'une propriété .value. Vue peut ainsi détecter les changements et synchroniser l'affichage automatiquement. Ainsi, dans la partie <script>, on lit/écrit la valeur via cette propriété .value (ex. : statutCommande.value = 'Expédiée'). Alors que dans la partie <template>, on utilise simplement le nom de la variable entre moustaches, sans .value (ex. : {{ statutCommande }}).
Types de données gérés par ref()
Comme nous venons de le voir, la fonction ref() permet de rendre réactives les valeurs primitives (chaîne de caractères, nombre, booléenne, etc.) mais elle peut aussi se charger de la réactivité des tableaux et des objets.
Réactivité des tableaux
Avec un tableau enveloppé dans ref([]), deux types d'action sont réactifs, c'est à dire qu'ils déclenchent la mise à jour de l'affichage :
- Mutations :
- Modifier : (ex.: items.value[1] = 42)
- Ajouter : (ex.: items.value.push(42))
- Supprimer : (ex.: items.value.pop())
- Trier : (ex.: items.value.sort())
- Vider : (ex.: items.value.length = 0)
- Réassignation de valeur à un nouveau tableau (ex. : items.value = ['X','Y']).
<script setup>
import { ref } from 'vue';
const items = ref(['A', 'B']);
// Mutation.
const ajouter = () => { items.value.push('C'); };
// Réassignation.
const remplacer = () => { items.value = ['X', 'Y', 'Z']; };
</script>
<template>
<p>{{ items }}</p>
<button type="button" @click="ajouter">Ajouter</button>
<button type="button" @click="remplacer">Remplacer</button>
</template>
Réactivité des objets
Tout comme pour les tableaux, avec un objet enveloppé dans ref({}), deux types d'action déclenchent la mise à jour de l'affichage :
- Mutations
- Modifier : produit.value.prix = 79
- Ajouter : produit.value.couleur = 'Noir'
- Supprimer : delete produit.value.couleur
- Réassignation de valeur à un nouvel objet (ex.: produit.value = { nom: 'Casque Pro', prix: 149 })
<script setup>
import { ref } from 'vue';
const produit = ref({ nom: 'Casque', prix: 99, couleur: 'rose'});
// Mutation.
const modifierCouleur = () => { produit.value.couleur = 'noir'; };
// Réassignation.
const remplacerProduit = () => { produit.value = { nom: 'Casque Pro', prix: 149 }; };
</script>
<template>
<p>{{ produit.nom }} {{ produit.couleur }} - {{ produit.prix }} €</p>
<button type="button" @click="modifierCouleur">Modifier couleur</button>
<button type="button" @click="remplacerProduit">Remplacer le produit</button>
</template>
Créer une donnée réactive avec reactive()
La fonction reactive() permet de créer des données réactives à partir d'objets ou de tableaux uniquement (à la différence de ref(), elle ne peut pas envelopper de valeurs primitives). Son intérêt principal est de rendre la manipulation de structures complexes plus naturelle, puisqu'il n'est plus nécessaire d'utiliser la propriété .value dans le bloc <script>. Les propriétés de l'objet ou les éléments du tableau peuvent alors être lus et modifiés comme en JavaScript classique.
<script setup>
import { reactive } from 'vue'
const produit = reactive({ nom: 'Casque', prix: 99, couleur: 'Rose' })
const appliquerPromo = () => { produit.prix = 79 }
const changerCouleur = () => { produit.couleur = 'Noir' }
</script>
<template>
<p>{{ produit.nom }} — {{ produit.couleur }} — {{ produit.prix }} €</p>
<button type="button" @click="appliquerPromo">Appliquer promo</button>
<button type="button" @click="changerCouleur">Changer couleur</button>
</template>
Quand il s'agit de choisir entre ref() et reactive(), il n'y a pas de règle stricte. Pour les valeurs primitives, ref() est incontournable. Pour les objets et les tableaux, les deux approches sont possibles. reactive() est souvent plus confortable car on manipule directement les propriétés ou les éléments, sans avoir à passer par .value.
La différence apparaît surtout lorsqu'on souhaite remplacer complètement un objet ou un tableau par un nouveau. Avec reactive(), la réassignation casse la réactivité, car la variable n'est plus suivie. Il faut alors modifier ses propriétés (par exemple avec Object.assign()). Avec ref(), au contraire, la réassignation est prévue et réactive (par exemple maRef.value = { ... }). C'est pourquoi certains développeurs préfèrent utiliser ref() lorsqu'ils savent qu'ils vont remplacer souvent une structure entière.
Les deux solutions offrent la même réactivité. Le choix dépend donc surtout de la manière dont les données évoluent dans le composant et du style d'écriture recherché.
Créer une donnée réactive avec shallowRef()
La fonction shallowRef() crée une référence réactive qui ne suit que la référence stockée, et non les mutations internes de l'objet ou du tableau contenu. L'interface n'est mise à jour que lors d'une réassignation complète de la valeur.
Ce comportement est particulièrement utile pour des données volumineuses ou des instances de bibliothèques tierces (graphiques, éditeurs, cartes…) dont l'état interne évolue fréquemment sans qu'il soit nécessaire d'en surveiller chaque détail. En limitant le suivi réactif à la seule référence, on améliore les performances du rendu et on évite des recalculs inutiles liés aux mutations internes.
Dans l'exemple suivant, le premier bouton modifie une propriété interne de l'objet stocké dans la shallowRef() et l'interface ne change pas, car la référence n'a pas été remplacée. Le second bouton remplace l'objet entier et l'interface se met à jour, car la référence a changé.
<script setup>
import { shallowRef } from 'vue';
// Référence réactive "peu profonde" : seule la référence changeante est suivie.
const preferences = shallowRef({
theme: 'clair',
notifications: true
});
// Mutation interne NON suivie : ne déclenche pas de mise à jour.
const changerTheme = () =>
{
preferences.value.theme = 'sombre';
};
// Remplacement complet SUIVI : déclenche une mise à jour.
const remplacerPreferences = () =>
{
preferences.value = { theme: 'sombre', notifications: false };
};
</script>
<template>
<p>Thème : {{ preferences.theme }}</p>
<button type="button" @click="changerTheme">Changer thème (mutation interne, non réactif)</button>
<button type="button" @click="remplacerPreferences">Remplacer préférences (réactif)</button>
</template>
Créer une donnée réactive avec shallowReactive()
La fonction shallowReactive() fonctionne de la même manière que reactive(), à une différence importante près, elle ne rend réactives que les propriétés de premier niveau de l'objet. Les objets ou tableaux contenus à l'intérieur ne sont pas transformés en proxys réactifs. Concrètement, Vue ne suit pas les modifications internes, mais seulement les réassignations de ces sous-objets de premier niveau uniquement.
Cette approche est utile lorsqu'on manipule des structures volumineuses (par exemple des objets JSON imbriqués ou des tableaux complexes), car Vue ne crée pas de proxys pour les sous-objets internes. On y gagne en performances lorsque seules les mutations ou réassignations de premier niveau doivent être suivies, sans observer chaque sous-propriété.
<script setup>
import { shallowReactive } from 'vue'
// Objet principal rendu réactif en surface uniquement
const utilisateur = shallowReactive({
nom: 'Alice',
preferences:
{
theme: 'clair',
notifications: true
}
})
// Cette fonction change une valeur interne (non suivie)
const changerTheme = () =>
{
utilisateur.preferences.theme = 'sombre'
}
// Cette fonction remplace tout l'objet interne (suivi)
const remplacerPreferences = () =>
{
utilisateur.preferences = { theme: 'sombre', notifications: false }
}
</script>
<template>
<p>Thème : {{ utilisateur.preferences.theme }}</p>
<button type="button" @click="changerTheme">Changer thème (non réactif)</button>
<button type="button" @click="remplacerPreferences">Remplacer préférences (réactif)</button>
</template>
Dans cet exemple, le premier bouton ne déclenche pas de mise à jour de l'interface, car Vue ne suit pas la propriété theme à l'intérieur de preferences. En revanche, lorsque tout l'objet preferences est remplacé, la réactivité fonctionne correctement, car la propriété de premier niveau a changé.
Désactiver la réactivité d'une propriété avec markRaw()
La fonction markRaw() permet d'exclure une donnée du système de réactivité de Vue. Autrement dit, les objets ou tableaux marqués avec cette fonction ne sont plus observés par Vue, leurs modifications internes n'entraînent donc aucun re-rendu du composant.
On utilise markRaw() lorsqu'une donnée ne doit pas être suivie par le système réactif, ou lorsque sa structure est trop complexe pour être observée sans impact sur les performances. C'est souvent le cas avec des bibliothèques externes (par exemple un graphique, une carte ou un objet 3D), ou avec des structures volumineuses dont la réactivité serait inutile et coûteuse en performances.
Cette approche permet d'éviter des calculs réactifs inutiles tout en gardant la flexibilité du reste de l'objet réactif. En revanche, les mutations internes d'un objet marqué comme « raw » ne seront plus détectées, il faudra réassigner la propriété entière si l'on souhaite que Vue mette à jour l'interface.
L'exemple suivant compare le comportement de deux listes contenues dans une même donnée réactive. La première reste entièrement réactive, tandis que la seconde a été configurée pour ne plus l'être. Concrètement, on a volontairement désactivé la réactivité de cette propriété afin d'observer la différence. Le premier bouton ajoute un élément à la liste réactive et met immédiatement l'affichage à jour. Le second agit sur la liste non réactive, dont l'affichage ne change pas. Enfin, un troisième bouton remplace complètement cette liste, ce qui force à nouveau la mise à jour de l'interface malgré la désactivation de sa réactivité interne.
<script setup>
import { reactive, markRaw } from 'vue';
// Création d'un objet principal réactif.
// Toutes ses propriétés deviennent observables, sauf si on les rend explicitement non réactives avec markRaw().
const donnees = reactive({
reactif: ['Article A', 'Article B'],
nonReactif: markRaw(['Élément X', 'Élément Y'])
});
// --- Mutation réactive ---
// On ajoute un élément à un tableau entièrement réactif.
// Vue détecte la mutation et met automatiquement à jour l'affichage.
const ajouterReactif = () =>
{
donnees.reactif.push('Nouvel article');
};
// --- Mutation non réactive ---
// On ajoute un élément à un tableau rendu non réactif avec markRaw().
// Vue ne suit plus ce tableau, donc l'affichage ne se mettra pas à jour.
// On utilise console.log() pour vérifier que la donnée interne change bien malgré tout.
const ajouterNonReactif = () =>
{
donnees.nonReactif.push('Nouvel élément');
console.log(donnees.nonReactif);
};
// --- Remplacement réactif d'une propriété ---
// On réassigne une nouvelle valeur à la propriété rendue non réactive avec markRaw().
// Les mutations internes du tableau ne sont pas suivies, mais le remplacement complet
// de la propriété est bien détecté par Vue. L'affichage se met donc à jour car
// la référence de la propriété change.
const remplacerNonReactif = () =>
{
donnees.nonReactif = markRaw(['Nouvelle liste A', 'Nouvelle liste B']);
};
</script>
<template>
<h4>Comparaison entre tableau réactif et tableau non réactif</h4>
<div>
<p><b>Tableau réactif :</b> {{ donnees.reactif.join(', ') }}</p>
<button @click="ajouterReactif">Ajouter un article (réactif)</button>
</div>
<div>
<p><b>Tableau non réactif :</b> {{ donnees.nonReactif.join(', ') }}</p>
<button @click="ajouterNonReactif">Ajouter un élément (non réactif)</button>
<button @click="remplacerNonReactif">Remplacer la liste (réactif)</button>
</div>
</template>
Cet exemple montre que la fonction markRaw() désactive uniquement la réactivité à l'intérieur de la structure qu'elle marque, sans affecter la réactivité de l'objet qui la contient. Ainsi, lorsqu'on ajoute ou supprime un élément dans le tableau non réactif, Vue n'en tient pas compte et l'affichage ne change pas. En revanche, si l'on remplace entièrement la propriété par une nouvelle référence, Vue détecte cette réassignation et met l'interface à jour.
Ce comportement permet de rendre certaines parties d'un objet réactif totalement neutres pour le système de suivi. Cela peut être utile pour stocker des données externes, volumineuses ou non pertinentes pour l'affichage, tout en évitant à Vue de surveiller inutilement leur contenu. Cette approche limite le nombre d'observations effectuées en arrière-plan et peut donc améliorer les performances lorsque les structures manipulées sont importantes.
Template et effets de bord
En dehors des fonctions associées à une interaction utilisateur (par exemple @click), il ne faut pas modifier une donnée réactive pendant l'évaluation du template, que ce soit par une mutation (ex. : compteur++) ou une réassignation (ex. : compteur = 5). Ce principe s'applique aussi bien aux expressions directes qu'aux fonctions appelées depuis le template. Les expressions utilisées pour produire l'affichage doivent rester pures, c'est-à-dire ne pas provoquer d'effet de bord.
Un effet de bord est une modification observable de l'état du programme ou de son environnement qui se produit en plus de la valeur éventuellement retournée par une fonction. Il peut s'agir, par exemple, de modifier une variable ou un objet existant, de manipuler directement le DOM, d'envoyer une requête réseau ou d'écrire dans le stockage local.
Exemple d'effet de bord dans le template
<script setup>
import { ref } from 'vue';
const compteur = ref(0);
// Mutation pendant le rendu.
function incrementerEtRetourner()
{
// nouvelle valeur à chaque rendu => nouveau rendu.
compteur.value++;
return compteur.value;
}
</script>
<template>
<h4>Boucle de rendu involontaire</h4>
<p>{{ incrementerEtRetourner() }}</p>
</template>
Cycle de rendu :
- Premier rendu.
- La fonction incrementerEtRetourner() est appelée et incrémente la valeur de compteur (de 0 à 1).
- Ce changement déclenche immédiatement un nouveau rendu du composant.
- Lors de ce nouveau rendu, la fonction est à nouveau exécutée et incrémente compteur (de 1 à 2).
- Le moteur réactif détecte à nouveau un changement et relance un rendu supplémentaire.
- Ce cycle se répète indéfiniment, chaque rendu provoque une nouvelle exécution de la fonction, qui elle-même modifie la donnée réactive et déclenche un nouveau rendu.
- Le navigateur finit par interrompre la boucle et Vue affiche une erreur : "Maximum recursive updates exceeded...".
Ce comportement s'explique par le mécanisme réactif de Vue. Chaque fois qu'une donnée réactive (créée avec ref() ou reactive()) est modifiée, Vue détecte automatiquement le changement et relance le rendu du composant afin de mettre à jour le contenu affiché dans le template. C'est ce principe de "synchronisation automatique" qui fait la puissance de Vue, mais aussi la raison pour laquelle toute écriture pendant le rendu doit être évitée.
Cas particulier de réassignation idempotente (réassignation dont la valeur ne change pas)
<script setup>
import { ref } from 'vue';
const compteur = ref(0);
// Réassignation pendant le rendu, mais valeur constante.
function fixerAUn()
{
compteur.value = 1;
return 'ok';
}
</script>
<template>
<h4>Pas de boucle mais mauvaise pratique</h4>
<p>{{ fixerAUn() }}</p>
</template>
Cycle de rendu :
- Premier rendu.
- La fonction fixerAUn() est appelée et la valeur de compteur, initialement à 0, est réassignée à 1.
- Un nouveau rendu est provoqué par le changement de la valeur de compteur.
- La fonction fixerAUn() est à nouveau appelée et la valeur de compteur, déjà à 1, est réassignée à 1.
- Vue ne déclenche pas de nouveau rendu, car la valeur réassignée est identique à la précédente (Object.is(1, 1) renvoie true), le moteur considère donc que rien n'a changé.
Ce code ne provoque pas de boucle, mais il demeure une mauvaise pratique.
Même si la valeur réassignée reste identique, Vue déclenche tout de même un nouveau rendu
avant de constater qu'aucun changement réel n'a eu lieu.
Cela entraîne un traitement inutile et peut provoquer des comportements incohérents
si, plus tard, la fonction commence à attribuer des valeurs différentes (ex.: date.now(), Math.random()).
En outre, écrire dans une donnée pendant le rendu va à l'encontre du principe de pureté du template où
chaque rendu doit se limiter à lire les données, pas à les modifier.
Créer une référence réactive vers une propriété avec toRef()
La fonction toRef() sert à créer une référence réactive liée à une propriété précise d'un objet réactif. Elle prend deux arguments :
- L'objet réactif d'origine.
- Le nom de la propriété cible, sous forme de chaîne de caractères.
Le résultat est une ref liée à la propriété d'origine. La propriété et la ref restent ainsi synchronisées.
Problème sans toRef
Lorsqu'on copie dans une variable une propriété contenant une valeur primitive d'un objet réactif, on récupère uniquement sa valeur actuelle. La nouvelle variable n'est donc plus liée à la propriété d'origine.
<script setup>
import { reactive } from 'vue'
const utilisateur = reactive({
nom: 'Dupont',
age: 32
})
// Copie de la valeur primitive contenue dans utilisateur.age.
const age = utilisateur.age
const vieillir = () => {
utilisateur.age++
}
</script>
<template>
<p>Nom : {{ utilisateur.nom }}</p>
<p>Âge extrait : {{ age }}</p>
<p>Âge d'origine : {{ utilisateur.age }}</p>
<button type="button" @click="vieillir">Vieillir</button>
</template>
Ici, age reçoit une copie de la valeur primitive contenue initialement dans utilisateur.age. Lorsque utilisateur.age est modifié, la variable age reste donc inchangée.
Cas des objets et des tableaux
Le comportement est différent lorsque la propriété contient un objet ou un tableau. Dans ce cas, la variable récupère une référence vers l'objet réactif.
const utilisateur = reactive({
loisirs: ['cinéma', 'lecture']
})
const loisirs = utilisateur.loisirs
Si on modifie le contenu du tableau, la modification reste réactive, car loisirs et utilisateur.loisirs désignent toujours le même tableau réactif.
utilisateur.loisirs.push('musique')
console.log(loisirs)
// ['cinéma', 'lecture', 'musique']
En revanche, si on remplace complètement la propriété par un nouveau tableau, la variable extraite continue de désigner l'ancien tableau.
utilisateur.loisirs = ['sport']
console.log(utilisateur.loisirs)
// ['sport']
console.log(loisirs)
// ['cinéma', 'lecture', 'musique']
La variable loisirs reste donc réactive vis-à-vis de l'ancien tableau, mais elle ne suit plus la propriété utilisateur.loisirs.
Solution avec toRef
La fonction toRef() permet de conserver un lien réactif avec la propriété elle-même, quelle que soit la valeur qu'elle contient.
<script setup>
import { reactive, toRef } from 'vue'
const utilisateur = reactive({
nom: 'Dupont',
age: 32
})
// Création d'une ref liée à la propriété 'age'.
const age = toRef(utilisateur, 'age')
const vieillir = () => {
age.value++
}
</script>
<template>
<p>Nom : {{ utilisateur.nom }}</p>
<p>Âge extrait : {{ age }}</p>
<p>Âge d'origine : {{ utilisateur.age }}</p>
<button type="button" @click="vieillir">Vieillir</button>
</template>
Grâce à toRef(utilisateur, 'age'), la variable age est directement liée à la propriété utilisateur.age.
Modifier age.value modifie donc utilisateur.age, et modifier utilisateur.age met également à jour age.
Ce principe reste valable si la propriété contient un objet ou un tableau : même si la propriété est remplacée par une nouvelle valeur, la ref continue de suivre cette propriété.
Déstructurer un objet tout en gardant la réactivité avec toRefs()
La fonction toRefs() crée une ref pour chacune des propriétés présentes d'un objet réactif. Chaque ref reste liée à la propriété correspondante de l'objet d'origine.
Elle est notamment utile lorsqu'on souhaite déstructurer un objet réactif sans perdre le lien avec ses propriétés.
Problème de la déstructuration classique
Une déstructuration classique extrait directement les valeurs contenues dans les propriétés de l'objet. Lorsque ces valeurs sont primitives, les variables obtenues ne restent donc pas liées aux propriétés de l'objet réactif.
import { reactive } from 'vue'
const utilisateur = reactive({
nom: 'Martin',
age: 27,
email: 'martin@example.com'
})
const { nom, age, email } = utilisateur
Ici, nom, age et email contiennent simplement les valeurs présentes dans l'objet au moment de la déstructuration.
Si utilisateur.age est ensuite modifié, la variable age ne sera pas mise à jour.
Solution avec toRefs()
La fonction toRefs() permet de créer une ref liée à chacune des propriétés avant de déstructurer l'objet.
<script setup>
import { reactive, toRefs } from 'vue'
const utilisateur = reactive({
nom: 'Martin',
age: 27,
email: 'martin@example.com'
})
// Création de refs liées aux propriétés de l'objet,
// puis déstructuration.
const { nom, age, email } = toRefs(utilisateur)
const vieillir = () => {
age.value++
}
</script>
<template>
<p>Nom : {{ nom }}</p>
<p>Âge : {{ age }}</p>
<p>Email : {{ email }}</p>
<button type="button" @click="vieillir">
Vieillir
</button>
</template>
Après l'appel à toRefs(), nom, age et email sont des refs.
La variable age reste ainsi liée à utilisateur.age. Modifier l'une modifie donc également l'autre.
Dans le script, il faut utiliser la propriété .value pour accéder ou modifier la valeur d'une ref. Dans le template, Vue déballe automatiquement les refs, ce qui permet d'écrire directement {{ age }}.
Et avec un tableau ?
La fonction toRefs() peut également être utilisée avec un tableau réactif. Elle crée alors des refs liées aux positions présentes dans le tableau.
const couleurs = reactive([
'rouge',
'vert',
'bleu'
])
const [premiere, deuxieme, troisieme] = toRefs(couleurs)
premiere.value = 'orange'
console.log(couleurs)
// ['orange', 'vert', 'bleu']
Dans la pratique, toRefs() est cependant surtout utilisé pour déstructurer les propriétés d'un objet réactif.
Récupérer un élément du DOM avec une ref de template
Dans certains cas, un composant doit interagir ponctuellement avec le DOM. Il peut s'agir de mesurer la taille d'un élément, déclencher un focus, lire une position ou encore effectuer un défilement. Un réflexe courant serait d'utiliser un sélecteur global comme document.querySelector(). Pourtant, cette pratique ne convient pas avec Vue. Vue contrôle entièrement le cycle de rendu et recrée des éléments du DOM à chaque mise à jour. Un sélecteur global risquerait donc de viser un élément rendu plus tôt, un élément encore absent, ou même un élément appartenant à un autre composant.
Pour ces besoins, Vue propose les refs de template. Elles fonctionnent comme les données réactives créées avec ref() mais elles stockent non pas une valeur métier, mais un élément du DOM. Vue remplit automatiquement ces refs après chaque rendu afin qu'elles pointent toujours vers l'élément réel et à jour. Cette approche est fiable, locale au composant et parfaitement intégrée au fonctionnement interne du framework.
Une ref de template se manipule dans le bloc <script> exactement comme une donnée réactive créée avec ref(). La différence se situe au moment de son initialisation. On commence par la créer avec ref(null), car aucun élément du DOM n'existe encore avant le rendu du template.
Lorsque Vue interprète le template, il repère l'attribut ref="nomDeLaRef" placé sur un élément. Dès que le rendu est terminé, Vue relie cet élément au nom déclaré dans le bloc script. À partir de ce moment, la propriété nomDeLaRef.value contient l'élément réel du DOM.
Une fois cette connexion établie, on peut utiliser les méthodes natives du navigateur sur l'élément récupéré. Il est alors possible d'appeler focus(), scrollIntoView() ou de lire offsetHeight selon les besoins du composant.
<script setup>
import { ref } from 'vue'
// Donnée réactive qui contiendra la hauteur en pixels.
const hauteurBloc = ref(0)
// Ref de template : Vue y placera l'élément DOM après le rendu.
const bloc = ref(null)
const mesurerHauteur = () =>
{
if (bloc.value)
{
// offsetHeight renvoie la hauteur réelle en pixels.
hauteurBloc.value = bloc.value.offsetHeight
}
}
</script>
<template>
<div
ref="bloc"
style="background:#eef;padding:12px;margin-bottom:8px;"
>
Contenu du bloc.
Cliquez sur le bouton pour mesurer ma hauteur.
</div>
<button type="button" @click="mesurerHauteur">
Mesurer la hauteur du bloc
</button>
<p>Hauteur mesurée {{ hauteurBloc }} px</p>
</template>
Une ref de template reste une ref à part entière, avec la même structure et la même propriété .value. La différence vient de son usage. Au lieu de stocker une donnée métier, elle contient un élément du DOM et permet de réaliser des actions impératives ponctuelles sans contourner le fonctionnement interne de Vue.
Notez que dans quelques cas particuliers, il reste possible d'utiliser les sélecteurs classiques du navigateur. Cela concerne uniquement des éléments que Vue ne contrôle pas, comme les balises globales <html> et <body>, ou encore les objets fournis par le navigateur tels que window et document. Ces éléments ne dépendent d'aucun composant, ils ne sont jamais recréés pendant le cycle de rendu et peuvent donc être ciblés avec les APIs classiques sans risque d'incohérence.
Exercices
Créer un nouveau projet Vue nommé vuejs03-la-reactivite en suivant la procédure décrite dans le chapitre dédié. Tous les exercices de cette page peuvent être réalisés dans ce projet, directement les uns à la suite des autres dans le fichier App.vue.
Dans la partie template, créer une balise <h1> avec le texte suivante : "Vue.js 03 : La réactivité".
Exercice 01 : Incrémentation / décrémentation
Objectif : Afficher et modifier une donnée réactive simple via des boutons.
Instructions :
- Dans la partie <script>, déclarer une variable nommée compteur, initialisée à 0 (pour la démonstration, ne pas utiliser la réactivité à ce stade de l'exercice).
-
Créer deux fonctions fléchées :
- incrementer() qui ajoute 1 à compteur.
- decrementer() qui enlève 1 à compteur.
- Dans la partie <template>, ajouter un sous-titre <h2> avec le texte "Exo 01".
- Ajouter un paragraphe qui affiche le texte "Compteur :" suivi de la valeur de compteur via des moustaches ({{ }}).
-
Ajouter deux boutons dans le template :
- Un bouton avec le texte "incrémenter" relié à @click="incrementer".
- Un bouton avec le texte "décrémenter" relié à @click="decrementer".
- Tester : cliquer plusieurs fois sur "incrémenter" et "décrémenter" et vérifier l'actualisation de la valeur dans la console. Constater que l'affichage dans la partie <template> ne se met pas à jour, la variable n'étant pas réactive.
-
Rendre la variable réactive pour actualiser aussi l'affichage dans le template :
- Dans la section <script>, importer la fonction ref depuis Vue.
- Remplacer la variable compteur par une constante réactive initialisée à 0 via la fonction ref().
- Adapter les fonctions du script pour lire/écrire la référence avec .value. Rappel : dans le template, utiliser simplement {{ compteur }} grâce à l'auto-déballage.
- Tester à nouveau : cliquer plusieurs fois sur "incrémenter" et "décrémenter" et vérifier que les mises à jour sont désormais visibles à la fois dans la console et dans la partie <template>.
Exercice 02 : Réassigner un objet réactif
Objectif : Découvrir la différence entre reactive() et ref() lorsqu'il faut remplacer un objet complet (idem pour les tableaux).
Instructions :
- Dans un premier temps, utiliser volontairement la fonction reactive(), qui ne permet pas de réassigner un objet complet. Pour cela, dans la partie <script>, importer reactive depuis Vue.
- Créer une constante produit avec reactive({...}) contenant les propriétés nom, prix, stock.
- Dans la partie <template>, ajouter un sous-titre <h2> avec le texte "Exo 02".
- Afficher la valeur des propriétés de l'objet à l'aide des moustaches, par exemple : "Produit : [nom] : [prix] € ([stock] en stock)".
-
Créer deux fonctions fléchées dans le script :
- chargerProduitA() qui réassigne produit avec un premier objet (ex. : "Clavier", 49, 12).
- chargerProduitB() qui réassigne produit avec un autre objet (ex. : "Souris", 29, 8).
- Ajouter deux boutons reliés à @click pour appeler ces fonctions.
-
Tester : cliquer sur les boutons. Constater que l'affichage dans le template
ne change pas et qu'un message d'erreur apparaît dans la console.
En effet, avec reactive(), Vue met à jour l'affichage quand on modifie les propriétés de l'objet, mais pas quand on essaie de remplacer l'objet complet par un autre. Dans ce cas, la variable produit perd la référence que Vue surveillait, donc l'affichage ne bouge pas. -
Corriger pour permettre la réassignation réactive :
- Remplacer l'importation de reactive par ref depuis Vue.
- Initialiser la constante réactive produit avec ref() plutôt que reactive().
- Adapter les fonctions pour réassigner (au sein de la partie <script>, il faut utiliser la propriété value sur les constantes réactives initialisées avec ref() pour pouvoir manipuler leur valeur).
- Tester à nouveau : cliquer sur les boutons doit remplacer entièrement l'objet affiché par le nouveau produit.
Exercice 03 : Modifier un tableau réactif (ajouter / retirer des éléments)
Objectif : Manipuler un tableau réactif en réponse à des événements utilisateur (ajout, suppression).
Instructions :
- Dans la partie <script>, importer la fonction reactive depuis Vue.
- Créer une constante réactive nommée panier initialisée avec reactive().
-
Créer trois fonctions fléchées :
- ajouterArticle() qui insère un nouvel élément dans panier en utilisant push(). Par exemple, ajouter le texte "Article X", où X correspond au nombre d'éléments déjà présents dans le tableau + 1 (calculé avec panier.length + 1).
- retirerDernier() qui supprime le dernier élément avec pop().
- viderPanier() qui vide le tableau en mettant sa longueur à 0 avec panier.length = 0.
- Dans la partie <template>, ajouter un sous-titre <h2> avec le texte "Exo 03".
- Ajouter un paragraphe qui affiche "Panier :" suivi de plusieurs valeurs du tableau avec des moustaches, par exemple {{ panier[0] }}, {{ panier[1] }}, etc. Si l'index n'existe pas encore (par exemple panier[2] alors que le tableau n'a qu'un seul élément), rien ne s'affichera.
-
Ajouter trois boutons reliés à des gestionnaires d'événements :
- Un bouton "Ajouter" relié à @click="ajouterArticle".
- Un bouton "Retirer dernier" relié à @click="retirerDernier".
- Un bouton "Vider" relié à @click="viderPanier".
- Tester : cliquer sur les boutons et vérifier que la liste affichée après "Panier :" se met à jour immédiatement.
La fonction reactive() est plus ergonomique pour les mutations de tableaux ou d'objets, d'autant plus quand la structure est profonde (objets imbriqués, tableaux dans des objets, etc.), car on peut modifier directement les propriétés ou les éléments sans avoir à manipuler la propriété value partout. Si l'on veut au contraire remplacer entièrement un tableau ou un objet par un nouveau, il est préférable d'utiliser ref() et de réassigner la valeur via la propriété value.