Cours Vue.js · Module 3: Réactivité
La réactivité dans Vue - Le réacteur à plasma vivant
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.valueen 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
toRefettoRefs, - diagnostiquer l'état avec
isRef,isReactive,unref,toRawetmarkRaw, - 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 changementPour 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 automatiquementDans 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 panneauL'é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. Qu'est-ce que la réactivité dans Vue.js ?
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().