Cours Vue.js · Module 3: Réactivité

La réactivité dans Vue - Le réacteur à plasma vivant

5 min de lecture
Dans cette leçon5

Bienvenue au réacteur à plasma du NOVA LAB ! Le Dr Nova te dit : ici, les capteurs prennent vie - tout comme les données dans Vue deviennent réactives et mettent automatiquement à jour l'interface de contrôle. Chaque lecture de puissance, de température ou de pression est instantanément visible sur les panneaux.

Dans le module 2, tu as écrit des templates qui se rafraîchissaient tout seuls après un changement des données, et tu l'as accepté sur parole. Dans ce module, nous regardons à l'intérieur de ce mécanisme, car sans le comprendre tu tomberas tôt ou tard sur un panneau qui affiche obstinément une ancienne mesure.

Ce que tu vas apprendre

  • expliquer ce qu'est la réactivité et comment Vue suit les changements de données,
  • créer des valeurs réactives avec ref() et utiliser .value en connaissance de cause,
  • construire des objets réactifs avec reactive() et contourner ses limites,
  • choisir une réactivité profonde ou superficielle : shallowRef, shallowReactive, triggerRef,
  • extraire des objets en toute sécurité avec toRef et toRefs,
  • diagnostiquer l'état avec isRef, isReactive, unref, toRaw et markRaw,
  • protéger l'état avec readonly() et l'organiser en une architecture claire.

Qu'est-ce que la réactivité ?

Commençons par le problème. En JavaScript pur, modifier une variable ne laisse aucune trace, donc l'écran n'a aucun moyen de l'apprendre :

1let powerLevel = 0
2document.querySelector('#power').textContent = powerLevel
3
4powerLevel = 75
5// l'écran affiche toujours 0 - personne ne sait qu'il y a eu un changement

Pour rafraîchir le panneau, tu devrais retrouver l'élément et y écrire la nouvelle valeur à la main après chaque changement. La réactivité est un système qui suit automatiquement les changements de données et met à jour le DOM. Quand tu modifies une valeur - comme le niveau de puissance du réacteur - Vue « le sait » et rafraîchit l'affichage sur le panneau de contrôle.

1<template>
2  <div class="reactor-panel" style="background: #0a0e27; color: #00ff88;">
3    <p>Statut : {{ reactorStatus }}</p>
4    <button @click="changeStatus">Changer le statut</button>
5  </div>
6</template>
7
8<script setup>
9import { ref } from 'vue'
10
11const reactorStatus = ref('STANDBY')
12
13function changeStatus() {
14  reactorStatus.value = 'ACTIVE'
15  // Vue mettra automatiquement à jour le panneau de contrôle !
16}
17</script>

Pendant le rendu, le template lit reactorStatus, et Vue note : ce composant dépend de ce capteur. Quand changeStatus affecte une nouvelle valeur, Vue refait le rendu du composant dépendant. Les mises à jour du DOM sont regroupées et appliquées au prochain « tick » (nextTick), donc dix changements d'affilée ne donnent qu'un seul rafraîchissement.

ref() - Le capteur réactif

ref() crée un « emballage » réactif pour une valeur - comme un capteur de réacteur qui surveille des paramètres. Elle prend une valeur initiale de n'importe quel type et renvoie un conteneur doté d'une propriété .value :

1import { ref } from 'vue'
2
3// Pour les primitives - lectures uniques
4const powerLevel = ref(0)
5const statusMessage = ref('Initialisation du système...')
6const isOnline = ref(true)
7
8// Pour les objets (fonctionne aussi) - données de capteur complexes
9const reactor = ref({
10  name: 'Plasma Core Alpha',
11  temperature: 2500
12})
13
14// Accéder à la valeur - utilise .value
15console.log(powerLevel.value) // 0
16powerLevel.value++
17console.log(powerLevel.value) // 1
18
19// Dans le template - pas besoin de .value !
20// {{ powerLevel }} - Vue le fait automatiquement

Dans le script, tu écris toujours .value. Les refs déclarés au niveau supérieur de <script setup> sont déballés automatiquement par le template, donc .value y est inutile. La variable reste const : c'est le contenu du conteneur qui change, pas le conteneur lui-même. Dans le module 2, un tel conteneur gardait aussi un élément du DOM - c'est toujours la même fonction, et c'est l'attribut ref qui y plaçait l'élément.

Pourquoi .value ?

JavaScript ne sait pas intercepter la lecture ou l'écriture d'une variable ordinaire, mais il sait intercepter l'accès à la propriété d'un objet. ref() enveloppe la valeur dans un objet doté d'une propriété .value - comme un capteur qui empaquette des données brutes pour les transmettre :

1const powerLevel = ref(0)
2
3// Sous le capot :
4// powerLevel = { value: 0 }
5
6// Modifier la valeur
7powerLevel.value = 75 // le setter de .value détecte le changement → mise à jour du panneau

L'écriture { value: 0 } est une simplification. En réalité, .value possède un getter qui enregistre qui lit la valeur, et un setter qui prévient ces lecteurs d'un changement. Les objets créés avec reactive() dans la prochaine leçon fonctionnent autrement : là, Vue utilise le mécanisme Proxy.

Typer ref (TypeScript)

Dans un composant avec <script setup lang="ts">, TypeScript déduit généralement seul le type d'un ref. Tu importes le type Ref<T> avec import type quand tu veux l'écrire explicitement, et l'interface Sensor décrit la forme des données du capteur :

1import { ref } from 'vue'
2import type { Ref } from 'vue'
3
4// Inférence de type automatique
5const temperature = ref(0) // Ref<number>
6const sensorName = ref('Temperature Alpha') // Ref<string>
7
8// Typage explicite
9const missionYear: Ref<number> = ref(2087)
10
11// Pour les types complexes
12interface Sensor {
13  name: string
14  unit: string
15  maxValue: number
16}
17
18const temperatureSensor = ref<Sensor>({
19  name: 'Plasma Core Temperature',
20  unit: 'Kelvin',
21  maxValue: 5000
22})

L'écriture ref<Sensor>() passe le type entre chevrons. Si tu indiques le type mais omets la valeur initiale, le type inclura aussi undefined.

La documentation de Vue recommande ref() comme outil principal pour déclarer l'état, et c'est aussi ainsi que je travaille. Dans la prochaine leçon, tu verras reactive(), qui couvre tout un panneau d'un coup, mais qui a ses propres limites.

Retiens : ref() est un capteur dans une capsule - dans le script, tu ouvres la capsule avec .value, et l'hologramme du template le fait pour toi.

Code de cette leçon : App.vue
1<script setup>
2import { ref, reactive } from 'vue'
3
4// Réacteur à plasma - Réactivité de base
5// ref() pour les valeurs primitives (nombres, chaînes, booléens)
6const reactorPower = ref(0)
7const isOnline = ref(false)
8const reactorStatus = ref('OFFLINE')
9
10// reactive() pour les objets
11const plasmaCore = reactive({
12  temperature: 0,
13  pressure: 0,
14  stability: 100,
15  fuelLevel: 100
16})
17
18function startReactor() {
19  isOnline.value = true
20  reactorStatus.value = 'INITIALIZING'
21
22  // Simulation du démarrage du réacteur
23  const interval = setInterval(() => {
24    if (reactorPower.value < 100) {
25      reactorPower.value += 10
26      plasmaCore.temperature += 150
27      plasmaCore.pressure += 5
28      plasmaCore.fuelLevel -= 2
29    } else {
30      reactorStatus.value = 'ONLINE'
31      clearInterval(interval)
32    }
33  }, 300)
34}
35
36function shutdownReactor() {
37  isOnline.value = false
38  reactorStatus.value = 'OFFLINE'
39  reactorPower.value = 0
40  plasmaCore.temperature = 0
41  plasmaCore.pressure = 0
42  plasmaCore.stability = 100
43  plasmaCore.fuelLevel = 100
44}
45</script>
46
47<template>
48  <div class="reactor-control">
49    <div class="header">
50      <h1>RÉACTEUR À PLASMA - NOVA LAB</h1>
51      <p class="subtitle">Système de réactivité Vue.js - ref() et reactive()</p>
52    </div>
53
54    <div class="status-grid">
55      <div class="status-card">
56        <div class="label">STATUT DU RÉACTEUR</div>
57        <div :class="['value', isOnline ? 'online' : 'offline']">
58          {{ reactorStatus }}
59        </div>
60      </div>
61
62      <div class="status-card">
63        <div class="label">PUISSANCE DE SORTIE</div>
64        <div class="value accent">{{ reactorPower }}%</div>
65        <div class="progress-bar">
66          <div class="progress-fill" :style="{ width: reactorPower + '%' }"></div>
67        </div>
68      </div>
69    </div>
70
71    <div class="plasma-grid">
72      <div class="plasma-card">
73        <div class="icon">◆</div>
74        <div class="label">TEMPÉRATURE</div>
75        <div class="value">{{ plasmaCore.temperature.toFixed(0) }}°C</div>
76      </div>
77
78      <div class="plasma-card">
79        <div class="icon">◆</div>
80        <div class="label">PRESSION</div>
81        <div class="value">{{ plasmaCore.pressure }} bar</div>
82      </div>
83
84      <div class="plasma-card">
85        <div class="icon">◆</div>
86        <div class="label">STABILITÉ</div>
87        <div class="value">{{ plasmaCore.stability }}%</div>
88      </div>
89
90      <div class="plasma-card">
91        <div class="icon">◆</div>
92        <div class="label">CARBURANT</div>
93        <div class="value">{{ plasmaCore.fuelLevel }}%</div>
94      </div>
95    </div>
96
97    <div class="controls">
98      <button
99        @click="startReactor"
100        :disabled="isOnline"
101        class="btn btn-start"
102      >
103        DÉMARRER LE RÉACTEUR
104      </button>
105      <button
106        @click="shutdownReactor"
107        :disabled="!isOnline"
108        class="btn btn-stop"
109      >
110        ARRÊTER LE RÉACTEUR
111      </button>
112    </div>
113
114    <div class="info-box">
115      <h3>Apprendre la réactivité</h3>
116      <p><strong>ref() :</strong> utilisé pour les valeurs primitives (reactorPower, isOnline)</p>
117      <p><strong>reactive() :</strong> utilisé pour les objets (plasmaCore)</p>
118      <p><strong>Accès :</strong> ref nécessite .value, reactive non</p>
119    </div>
120  </div>
121</template>
122
123<style scoped>
124.reactor-control {
125  min-height: 100vh;
126  background: #0a0e27;
127  color: #e0e0e0;
128  padding: 2rem;
129  font-family: 'Courier New', monospace;
130}
131
132.header {
133  text-align: center;
134  margin-bottom: 2rem;
135  border-bottom: 2px solid #00ff88;
136  padding-bottom: 1rem;
137}
138
139.header h1 {
140  color: #00ff88;
141  font-size: 2rem;
142  margin: 0;
143  text-shadow: 0 0 10px #00ff88;
144}
145
146.subtitle {
147  color: #00b4d8;
148  margin: 0.5rem 0 0 0;
149}
150
151.status-grid {
152  display: grid;
153  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
154  gap: 1rem;
155  margin-bottom: 2rem;
156}
157
158.status-card {
159  background: #1a1f3a;
160  padding: 1.5rem;
161  border-radius: 8px;
162  border: 1px solid #00b4d8;
163}
164
165.label {
166  font-size: 0.9rem;
167  color: #888;
168  margin-bottom: 0.5rem;
169}
170
171.value {
172  font-size: 1.8rem;
173  font-weight: bold;
174  color: #fff;
175}
176
177.value.online {
178  color: #00ff88;
179  text-shadow: 0 0 10px #00ff88;
180}
181
182.value.offline {
183  color: #ff4444;
184}
185
186.value.accent {
187  color: #00b4d8;
188}
189
190.progress-bar {
191  height: 8px;
192  background: #0a0e27;
193  border-radius: 4px;
194  margin-top: 0.5rem;
195  overflow: hidden;
196}
197
198.progress-fill {
199  height: 100%;
200  background: linear-gradient(90deg, #00b4d8, #00ff88);
201  transition: width 0.3s ease;
202}
203
204.plasma-grid {
205  display: grid;
206  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
207  gap: 1rem;
208  margin-bottom: 2rem;
209}
210
211.plasma-card {
212  background: #1a1f3a;
213  padding: 1.5rem;
214  border-radius: 8px;
215  border: 1px solid #333;
216  text-align: center;
217}
218
219.plasma-card .icon {
220  font-size: 2rem;
221  margin-bottom: 0.5rem;
222}
223
224.plasma-card .value {
225  font-size: 1.5rem;
226  color: #00ff88;
227}
228
229.controls {
230  display: flex;
231  gap: 1rem;
232  margin-bottom: 2rem;
233  justify-content: center;
234}
235
236.btn {
237  padding: 1rem 2rem;
238  font-size: 1.1rem;
239  border: none;
240  border-radius: 8px;
241  cursor: pointer;
242  font-weight: bold;
243  transition: all 0.3s ease;
244  font-family: 'Courier New', monospace;
245}
246
247.btn-start {
248  background: linear-gradient(135deg, #00ff88, #00b4d8);
249  color: #0a0e27;
250}
251
252.btn-start:hover:not(:disabled) {
253  transform: scale(1.05);
254  box-shadow: 0 0 20px #00ff88;
255}
256
257.btn-stop {
258  background: linear-gradient(135deg, #ff4444, #ff6666);
259  color: white;
260}
261
262.btn-stop:hover:not(:disabled) {
263  transform: scale(1.05);
264  box-shadow: 0 0 20px #ff4444;
265}
266
267.btn:disabled {
268  opacity: 0.5;
269  cursor: not-allowed;
270}
271
272.info-box {
273  background: #1a1f3a;
274  padding: 1.5rem;
275  border-radius: 8px;
276  border-left: 4px solid #00ff88;
277}
278
279.info-box h3 {
280  color: #00ff88;
281  margin-top: 0;
282}
283
284.info-box p {
285  margin: 0.5rem 0;
286  color: #ccc;
287}
288
289.info-box strong {
290  color: #00b4d8;
291}
292</style>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Qu'est-ce que la réactivité dans Vue.js ?

  2. 2. À quoi sert ref() dans Vue 3 ?

Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée des variables réactives avec ref() pour le nom de la station et la température, et affiche-les dans le template.

  • Clique dans l'ordre

    Range dans le bon ordre les étapes de création et d'utilisation de ref().

  • Éditeur de code

    Crée un compteur avec des boutons d'incrémentation et de décrémentation en utilisant ref().

  • Tri vertical

    Range dans le bon ordre les étapes de création d'un état réactif avec ref().