frontend / vuejs
Réactivité — ref, reactive, toRefs
Explication
Ce que vous allez apprendre
- Comprendre comment Vue 3 détecte les changements grâce aux Proxy JavaScript
- Choisir entre
ref()etreactive()selon le type de donnée à représenter - Identifier pourquoi déstructurer un objet
reactive()casse silencieusement sa réactivité - Réparer ce problème avec
toRefs()sans perdre la réactivité - Utiliser
shallowRef/shallowReactivepour optimiser de grosses structures de données
Dans quel contexte ?
Un développeur extrait la logique d'un compteur dans une fonction réutilisable (un composable, vu en détail plus tard dans ce cours) et remarque que la valeur retournée ne se met plus à jour dans l'interface une fois déstructurée dans le composant appelant. Ce bug, extrêmement courant chez les débutants Vue 3, vient d'une mauvaise compréhension de la différence entre ref() et reactive() — exactement le sujet de cette leçon.
D'abord, comment Vue sait qu'une valeur a changé
Vue 3 repose sur les Proxy JavaScript natifs : un objet enveloppé par reactive() devient un proxy qui intercepte chaque lecture et écriture, permettant à Vue de savoir précisément quelles parties de l'interface dépendent de quelles données, et de ne mettre à jour que ce qui a réellement changé.
ref() fonctionne différemment : il enveloppe n'importe quelle valeur (primitive ou objet) dans un objet simple avec une propriété .value, qui elle-même est rendue réactive.
Prérequis
Une compréhension de base de ref() (leçon 1) est indispensable ici : cette leçon en approfondit le fonctionnement et introduit son complément, reactive().
Ensuite, quand utiliser lequel
ref() convient à toute valeur, mais s'impose surtout pour des primitives (nombre, chaîne, booléen) ou quand on veut pouvoir réassigner toute la valeur d'un coup. reactive() est plus naturel pour des objets ou des tableaux qu'on mute directement (etat.utilisateur.age++), sans jamais avoir besoin de réassigner l'objet entier.
| Cas d'usage | Choix recommandé |
|---|---|
| Une valeur primitive (nombre, texte, booléen) | ref() |
| Un objet muté en place, jamais réassigné entièrement | reactive() |
| Besoin de réassigner toute la valeur d'un coup | ref() |
| Un objet destiné à être déstructuré ailleurs | ref() (ou toRefs() sur un reactive) |
Il reste un piège qui surprend presque tout le monde au début
reactive() fonctionne par proxy sur l'objet complet. Déstructurer cet objet (const { taches } = etat) extrait la VALEUR à cet instant précis, en rompant le lien avec le proxy d'origine — la variable taches locale ne réagira plus jamais aux changements de etat.taches.
Piège fréquent
reactive() ne peut également jamais être réassigné en entier (etat = reactive({...}) perdrait toute réactivité) : contrairement à ref(), seule la MUTATION de ses propriétés internes déclenche des mises à jour, jamais le remplacement de la variable elle-même.
Maintenant, comment réparer ce problème de déstructuration
toRefs(etat) convertit chaque propriété d'un objet reactive() en une ref individuelle, chacune restant liée à la propriété d'origine. C'est exactement le pattern utilisé dans un composable qui retourne un état interne reactive() : return { ...toRefs(etat), incrementer } garantit que le composant appelant garde la réactivité même après déstructuration.
Astuce
isRef(), unref() et toRaw() sont des utilitaires précieux pour du code générique qui doit fonctionner avec des refs ou des valeurs brutes indifféremment. toRaw() en particulier est utile pour passer un objet "nu" à une bibliothèque externe qui ne comprend pas les proxies Vue.
Enfin, une optimisation pour les grosses structures
shallowRef/shallowReactive limitent la réactivité au premier niveau seulement, évitant le coût de rendre TOUTE une structure profonde réactive quand seule sa référence globale change réellement (par exemple, un jeu de données de 100 000 lignes remplacé entièrement plutôt que muté ligne par ligne).
Maintenant que la mécanique de réactivité de Vue n'a plus de secret, la prochaine leçon compare les deux façons d'écrire un composant : l'Options API historique et la Composition API moderne, qui s'appuie directement sur ref() et reactive().
Commandes & code
Réactivité — ref, reactive, toRefs
Le système de réactivité de Vue 3 repose sur des Proxy JavaScript.
import { ref, reactive, toRefs, toRaw, isRef, unref, readonly } from 'vue'
// ref() : enveloppe n'importe quelle valeur (primitive ou objet) dans un objet { value }
const compteur = ref(0)
compteur.value++ // accès/modif via .value en dehors du template
console.log(compteur.value) // 1
// reactive() : rend un OBJET réactif directement (proxy), pas de .value
const etat = reactive({
utilisateur: { nom: 'Bob', age: 30 },
taches: [],
})
etat.utilisateur.age++ // fonctionne : le proxy intercepte l'accès
etat.taches.push({ id: 1, titre: 'Apprendre Vue' })
// PIÈGE n°1 : reactive() perd sa réactivité si on déstructure
const { utilisateur } = etat
// utilisateur.nom = 'Carl' <-- fonctionne encore (objet imbriqué reste proxy)
const { taches } = etat
// taches = [] <-- CASSE la réactivité : on remplace la référence, pas le contenu du proxy
// PIÈGE n°2 : reactive() ne peut PAS être réassigné entièrement
// etat = reactive({ ... }) <-- perdrait la réactivité, préférer Object.assign(etat, nouvellesDonnees)// toRefs() : convertit chaque propriété d'un objet reactive en ref individuelle,
// utile pour déstructurer un état reactive() SANS perdre la réactivité.
function useCompteur() {
const etat = reactive({ valeur: 0, pas: 1 })
function incrementer() {
etat.valeur += etat.pas
}
// Sans toRefs, `const { valeur } = etat` casserait la réactivité au retour de la fonction.
return { ...toRefs(etat), incrementer }
}
// Côté composant :
// const { valeur, pas, incrementer } = useCompteur()
// valeur reste réactif car c'est désormais une vraie ref liée à etat.valeur// Utilitaires complémentaires
const r = ref(42)
console.log(isRef(r)) // true
console.log(unref(r)) // 42 -- équivalent à : isRef(r) ? r.value : r
const etatBrut = toRaw(reactive({ x: 1 })) // objet JS "nu", sans proxy (perf, interop lib externe)
// readonly() : empêche toute mutation (utile pour exposer un état sans permettre sa modification)
const configLectureSeule = readonly({ apiUrl: 'https://api.exemple.com' })
// configLectureSeule.apiUrl = 'x' -> avertissement en dev, mutation ignorée// shallowRef / shallowReactive : réactivité seulement au premier niveau,
// utile pour de grosses structures où le suivi profond coûte cher en performance.
import { shallowRef, triggerRef } from 'vue'
const grosDataset = shallowRef({ lignes: new Array(100000).fill(0) })
function remplacerDataset(nouvellesLignes) {
grosDataset.value = { lignes: nouvellesLignes } // déclenche la réactivité (réassignation de .value)
}
function muterSansDeclencher() {
grosDataset.value.lignes.push(1) // NE déclenche PAS de mise à jour (mutation interne non suivie)
triggerRef(grosDataset) // force manuellement la mise à jour si nécessaire
}Résumé
ref()pour les valeurs primitives (ou quand on veut réassigner toute la valeur) ; accès via.valueen JS.reactive()pour les objets/tableaux, mais attention à la déstructuration qui casse la réactivité.toRefs()répare ce problème en convertissant chaque champ enrefindividuelle.shallowRef/shallowReactive: optimisation pour de grosses structures peu mutées en profondeur.
Exercices pratiques
Mission : sauver un composable qui a perdu sa réactivité
Objectif : Diagnostiquer une perte de réactivité après déstructuration d'un `reactive()`, puis la corriger avec `toRefs`.
Contexte
Un composable useCompteur() déclare en interne const etat = reactive({ valeur: 0, pas: 1 }) et retourne actuellement return { ...etat, incrementer }. Dans le composant appelant, const { valeur, incrementer } = useCompteur() : cliquer sur un bouton appelle bien incrementer(), la console confirme que etat.valeur augmente à l'intérieur du composable, mais le chiffre affiché dans le template du composant reste bloqué à 0.