Cours Vue.js · Module 3: Réactivité
Déboguer la réactivité
Dans cette leçon6
Même les meilleurs ingénieurs du NOVA LAB doivent parfois déboguer leurs systèmes. Découvre les outils pour déboguer la réactivité.
« Le panneau ne se rafraîchit pas » est le signalement le plus fréquent du journal des pannes de la station. La cause se trouve presque toujours dans les données : la valeur s'avère être un objet ordinaire au lieu d'un Proxy, ou elle a perdu sa connexion après une déstructuration. Avant de réécrire le composant, vérifie avec quoi tu travailles vraiment - Vue a pour cela un jeu de testeurs de diagnostic.
isRef() et isReactive()
isRef() renvoie true quand la valeur est un ref. isReactive() vérifie si un objet est un Proxy créé par reactive() ou shallowReactive(). Les deux fonctions renvoient un booléen :
1import { ref, reactive, isRef, isReactive } from 'vue'
2
3const powerLevel = ref(0)
4const reactor = reactive({ name: 'Plasma Core Alpha' })
5const plainConfig = { mode: 'safe' }
6
7console.log(isRef(powerLevel)) // true
8console.log(isRef(reactor)) // false
9console.log(isReactive(reactor)) // true
10console.log(isReactive(powerLevel)) // false
11console.log(isReactive(plainConfig)) // falseUn ref n'est pas un Proxy, c'est pourquoi isReactive(powerLevel) donne false. En revanche, un objet rangé dans un ref en est un : isReactive(ref({}).value) renvoie true. Les testeurs voisins sont isProxy(), qui reconnaît tout Proxy de Vue, y compris en lecture seule, et isReadonly().
unref() - Extraction sûre d'une lecture
unref(x) est un raccourci pour isRef(x) ? x.value : x. Il permet d'écrire une fonction qui accepte aussi bien un ref qu'une valeur ordinaire :
1import { ref, unref } from 'vue'
2
3const temperature = ref(2500)
4const plainReading = 1500
5
6// unref renvoie .value pour un ref, ou la valeur directement
7console.log(unref(temperature)) // 2500
8console.log(unref(plainReading)) // 1500
9
10// Utile dans les fonctions universelles
11function convertToFahrenheit(kelvin) {
12 return (unref(kelvin) - 273.15) * 9/5 + 32
13}
14
15convertToFahrenheit(temperature) // 4040.3299999999995 (env. 4040.33)
16convertToFahrenheit(plainReading) // 2240.33La longue traîne après la virgule vient de l'arithmétique à virgule flottante de JavaScript, pas de Vue - pour l'affichage, utilise toFixed(2). Depuis Vue 3.3, il existe aussi toValue(), qui fonctionne comme unref() et appelle en plus les getters qu'on lui passe.
toRaw() - L'objet capteur brut
toRaw() renvoie l'objet original caché derrière le Proxy. C'est une échappatoire : les lectures par ce biais ne sont pas suivies, et les écritures ne rafraîchissent pas la vue.
1import { reactive, toRaw } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500
6})
7
8const rawReactor = toRaw(reactor)
9console.log(rawReactor) // Objet original sans Proxy
10
11// Utile pour :
12// - Comparer des objets
13// - Passer des données à des API qui refusent un Proxy (ex. structuredClone)
14// - Les bibliothèques externesJSON.stringify() gère un Proxy sans problème, mais structuredClone() ou postMessage() lèvent une DataCloneError - donne-leur plutôt toRaw(reactor). La documentation déconseille de garder une référence durable vers l'objet brut, car il est facile d'y faire des changements que le panneau ne remarquera jamais.
markRaw() - Désactiver la réactivité
markRaw() marque un objet pour que Vue ne le transforme jamais en Proxy, et renvoie ce même objet :
1import { reactive, markRaw } from 'vue'
2
3const externalSensorLibrary = markRaw({
4 hardwareInterface: new Map(),
5 calibrate() { /* ... */ }
6})
7
8const state = reactive({
9 sensor: externalSensorLibrary // Ne sera pas réactif
10})
11
12// Utile pour :
13// - Les bibliothèques matérielles (pilotes de capteurs, interfaces matérielles)
14// - Les gros ensembles de données immuables de la base de données du NOVA LAB
15// - Les objets de composants VueisReactive(state.sensor) renvoie false, et state.sensor est exactement le même objet que externalSensorLibrary. Un Proxy peut même casser certaines classes de bibliothèques : une méthode qui accède à un champ privé écrit avec un dièse, par exemple #points, lève alors une TypeError. markRaw l'évite. Attention : la marque ne concerne que l'objet lui-même, pas ses champs imbriqués.
Déboguer avec watch
watch(source, callback) appelle une fonction avec la nouvelle et l'ancienne valeur après chaque changement de la source, et l'option immediate: true la lance tout de suite. Nous verrons les watchers dans le module 4, mais ils peuvent déjà aider l'enquête :
1import { ref, watch } from 'vue'
2
3const count = ref(0)
4
5watch(count, (newValue, oldValue) => {
6 console.log(`count a changé : ${oldValue} → ${newValue}`)
7 console.trace() // Trace de pile - d'où vient le changement ?
8}, { immediate: true })La première entrée est count a changé : undefined → 0, car au démarrage il n'y a pas encore d'ancienne valeur. console.trace() affiche la pile d'appels, donc tu vois quelle fonction a modifié la lecture. En mode développement, Vue propose aussi les options onTrack et onTrigger pour les watchers et les computed, ainsi que les hooks onRenderTracked et onRenderTriggered.
Vue DevTools
Vue DevTools est une extension de navigateur, disponible aussi sous forme de plugin Vite. Dans les Vue DevTools, tu peux :
- Voir toutes les données réactives
- Suivre les changements en temps réel
- Éditer les valeurs en direct
- Voir l'arbre des dépendances
L'onglet des composants montre l'arbre des composants avec leur état, la Timeline enregistre les événements et les temps de rendu, et l'onglet Graph montre les dépendances réactives du composant sélectionné. Installe-les dès le premier jour - elles t'éviteront des heures de devinettes. Dans la prochaine leçon, tu assembleras tous les outils appris en panneaux pratiques.
Retiens : avant de démonter le réacteur, vérifie-le au testeur - isRef et isReactive te disent si le câble est seulement branché.
Code de cette leçon : App.vue
1<script setup>
2import { ref, reactive, shallowRef, shallowReactive, readonly, triggerRef } from 'vue'
3
4// Réactivité avancée - shallow et readonly
5// Système de protection des données de mission NOVA LAB
6
7// shallowRef - réactivité uniquement au niveau .value
8const missionData = shallowRef({
9 id: 'MISSION-2087',
10 crew: ['Chen', 'Rodriguez', 'Kim'],
11 objectives: {
12 primary: 'Establish Mars base',
13 secondary: 'Collect samples'
14 }
15})
16
17// shallowReactive - réactivité uniquement au premier niveau
18const missionStatus = shallowReactive({
19 name: 'Mars Colony Alpha',
20 phase: 'DEPLOYMENT',
21 timeline: {
22 start: '2087-03-15',
23 current: '2087-06-20',
24 estimated_end: '2087-12-31'
25 },
26 modules: {
27 habitation: { status: 'ACTIVE', progress: 75 },
28 power: { status: 'ACTIVE', progress: 90 },
29 lifesupport: { status: 'TESTING', progress: 60 }
30 }
31})
32
33// readonly - lecture seule
34const criticalParameters = readonly(reactive({
35 maxCrewSize: 12,
36 minOxygen: 21,
37 maxRadiation: 100,
38 safetyProtocols: {
39 emergency: 'AUTO_SHUTDOWN',
40 backup: 'REDUNDANT_SYSTEMS'
41 }
42}))
43
44// Exemple de ref normal pour comparaison
45const normalMissionData = ref({
46 id: 'MISSION-2087',
47 crew: ['Chen', 'Rodriguez', 'Kim']
48})
49
50// Démonstration de shallowRef
51function modifyShallowRef() {
52 // Ceci NE marchera PAS - changement d'une propriété profondément imbriquée
53 missionData.value.objectives.primary = 'Build greenhouse'
54 console.log("Propriété imbriquée modifiée, mais l'interface ne se mettra pas à jour automatiquement")
55
56 // On doit appeler triggerRef manuellement
57 // triggerRef(missionData)
58}
59
60function replaceShallowRef() {
61 // Ceci marchera - on remplace le .value entier
62 missionData.value = {
63 ...missionData.value,
64 objectives: {
65 primary: 'Build greenhouse',
66 secondary: 'Test farming systems'
67 }
68 }
69}
70
71function forceUpdateShallowRef() {
72 // Modification d'une propriété imbriquée
73 missionData.value.crew.push('Jackson')
74 // On force manuellement la mise à jour
75 triggerRef(missionData)
76}
77
78// Démonstration de shallowReactive
79function modifyShallowReactive() {
80 // Ceci marchera - changement au premier niveau
81 missionStatus.phase = 'OPERATIONAL'
82}
83
84function modifyDeepShallowReactive() {
85 // Ceci NE marchera PAS automatiquement - changement plus profond
86 missionStatus.modules.habitation.progress = 100
87 console.log("Propriété profonde modifiée, l'interface ne se mettra pas à jour")
88}
89
90function replaceNestedShallowReactive() {
91 // Ceci marchera - on remplace l'objet au premier niveau
92 missionStatus.modules = {
93 ...missionStatus.modules,
94 habitation: { status: 'COMPLETE', progress: 100 }
95 }
96}
97
98// Démonstration de readonly
99function tryModifyReadonly() {
100 try {
101 // Ceci NE marchera PAS - readonly bloque les modifications
102 criticalParameters.maxCrewSize = 20
103 } catch (e) {
104 console.warn('Cannot modify readonly object')
105 }
106}
107
108// Compteurs pour la démonstration
109const shallowRefCounter = ref(0)
110const shallowReactiveCounter = ref(0)
111const readonlyCounter = ref(0)
112</script>
113
114<template>
115 <div class="advanced-reactivity">
116 <div class="header">
117 <h1>SYSTÈME DE PROTECTION DES DONNÉES - NOVA LAB</h1>
118 <p class="subtitle">shallowRef(), shallowReactive(), readonly(), triggerRef()</p>
119 </div>
120
121 <!-- Démo shallowRef -->
122 <div class="demo-section">
123 <h2>shallowRef() - Réactivité superficielle</h2>
124 <div class="demo-grid">
125 <div class="info-card">
126 <h3>Caractéristiques</h3>
127 <ul>
128 <li>Réactif uniquement au niveau <code>.value</code></li>
129 <li>Les changements de propriétés profondément imbriquées ne déclenchent PAS de mise à jour</li>
130 <li>Utilise <code>triggerRef()</code> pour forcer une mise à jour</li>
131 <li>Plus efficace pour les gros objets</li>
132 </ul>
133 </div>
134
135 <div class="data-card">
136 <h3>Données de mission (shallowRef)</h3>
137 <pre>{{ JSON.stringify(missionData, null, 2) }}</pre>
138
139 <div class="controls">
140 <button @click="modifyShallowRef(); shallowRefCounter++" class="btn btn-warning">
141 Modifier une imbriquée (ne marchera pas)
142 </button>
143 <button @click="replaceShallowRef(); shallowRefCounter++" class="btn btn-success">
144 Remplacer .value (marchera)
145 </button>
146 <button @click="forceUpdateShallowRef(); shallowRefCounter++" class="btn btn-info">
147 Forcer la mise à jour (triggerRef)
148 </button>
149 </div>
150
151 <div class="counter">
152 Nombre d'actions : {{ shallowRefCounter }}
153 </div>
154 </div>
155 </div>
156
157 <div class="code-example">
158 <h4>Exemple de code :</h4>
159 <pre>const data = shallowRef({ nested: { value: 1 } })
160
161// Ne déclenchera pas de mise à jour de l'interface
162data.value.nested.value = 2
163
164// Déclenchera une mise à jour de l'interface
165data.value = { nested: { value: 2 } }
166
167// Forcer la mise à jour après modification
168data.value.nested.value = 2
169triggerRef(data)</pre>
170 </div>
171 </div>
172
173 <!-- Démo shallowReactive -->
174 <div class="demo-section">
175 <h2>shallowReactive() - Réactivité superficielle pour les objets</h2>
176 <div class="demo-grid">
177 <div class="info-card">
178 <h3>Caractéristiques</h3>
179 <ul>
180 <li>Seules les propriétés de premier niveau sont réactives</li>
181 <li>Les objets imbriqués ne sont PAS réactifs</li>
182 <li>Les objets imbriqués doivent être remplacés entièrement</li>
183 <li>Efficace pour les structures plates</li>
184 </ul>
185 </div>
186
187 <div class="data-card">
188 <h3>Statut de la mission (shallowReactive)</h3>
189 <div class="status-display">
190 <p><strong>Nom :</strong> {{ missionStatus.name }}</p>
191 <p><strong>Phase :</strong>
192 <span class="phase-badge">{{ missionStatus.phase }}</span>
193 </p>
194 <p><strong>Timeline :</strong> {{ missionStatus.timeline.current }}</p>
195
196 <div class="modules-grid">
197 <div
198 v-for="(module, key) in missionStatus.modules"
199 :key="key"
200 class="module-card"
201 >
202 <div class="module-name">{{ key }}</div>
203 <div class="module-status">{{ module.status }}</div>
204 <div class="progress-bar">
205 <div class="progress-fill" :style="{ width: module.progress + '%' }">
206 {{ module.progress }}%
207 </div>
208 </div>
209 </div>
210 </div>
211 </div>
212
213 <div class="controls">
214 <button @click="modifyShallowReactive(); shallowReactiveCounter++" class="btn btn-success">
215 Changer la phase (1er niveau)
216 </button>
217 <button @click="modifyDeepShallowReactive(); shallowReactiveCounter++" class="btn btn-warning">
218 Changer la progression (profond)
219 </button>
220 <button @click="replaceNestedShallowReactive(); shallowReactiveCounter++" class="btn btn-success">
221 Remplacer le module
222 </button>
223 </div>
224
225 <div class="counter">
226 Nombre d'actions : {{ shallowReactiveCounter }}
227 </div>
228 </div>
229 </div>
230
231 <div class="code-example">
232 <h4>Exemple de code :</h4>
233 <pre>const state = shallowReactive({
234 count: 0,
235 nested: { value: 1 }
236})
237
238// Marchera - premier niveau
239state.count = 1
240
241// Ne marchera pas - imbriqué
242state.nested.value = 2
243
244// Marchera - remplacement
245state.nested = { value: 2 }</pre>
246 </div>
247 </div>
248
249 <!-- Démo readonly -->
250 <div class="demo-section">
251 <h2>readonly() - Données en lecture seule</h2>
252 <div class="demo-grid">
253 <div class="info-card">
254 <h3>Caractéristiques</h3>
255 <ul>
256 <li>Protection complète contre la modification</li>
257 <li>Idéal pour les configurations constantes</li>
258 <li>Empêche les changements accidentels</li>
259 <li>Affiche des avertissements en mode dev</li>
260 </ul>
261 </div>
262
263 <div class="data-card">
264 <h3>Paramètres critiques (readonly)</h3>
265 <div class="critical-params">
266 <div class="param-item">
267 <span class="param-label">Équipage max :</span>
268 <span class="param-value">{{ criticalParameters.maxCrewSize }}</span>
269 </div>
270 <div class="param-item">
271 <span class="param-label">Oxygène min (%) :</span>
272 <span class="param-value">{{ criticalParameters.minOxygen }}</span>
273 </div>
274 <div class="param-item">
275 <span class="param-label">Radiation max :</span>
276 <span class="param-value">{{ criticalParameters.maxRadiation }} mSv</span>
277 </div>
278 <div class="param-item">
279 <span class="param-label">Protocole d'urgence :</span>
280 <span class="param-value">{{ criticalParameters.safetyProtocols.emergency }}</span>
281 </div>
282 <div class="param-item">
283 <span class="param-label">Système de secours :</span>
284 <span class="param-value">{{ criticalParameters.safetyProtocols.backup }}</span>
285 </div>
286 </div>
287
288 <div class="controls">
289 <button @click="tryModifyReadonly(); readonlyCounter++" class="btn btn-danger">
290 Tenter une modification (ne marchera pas)
291 </button>
292 </div>
293
294 <div class="counter">
295 Tentatives de modification échouées : {{ readonlyCounter }}
296 </div>
297
298 <div class="warning-box">
299 Les paramètres critiques sont protégés contre la modification
300 </div>
301 </div>
302 </div>
303
304 <div class="code-example">
305 <h4>Exemple de code :</h4>
306 <pre>const config = readonly(reactive({
307 apiKey: 'secret',
308 maxRetries: 3
309}))
310
311// Ne marchera pas - readonly
312config.maxRetries = 5 // Avertissement dans la console
313
314// On ne peut que lire
315console.log(config.maxRetries) // 3</pre>
316 </div>
317 </div>
318
319 <div class="info-box">
320 <h3>Quand utiliser les fonctions avancées ?</h3>
321 <div class="use-cases">
322 <div class="use-case">
323 <h4>shallowRef()</h4>
324 <p>Gros objets dont tu changes l'objet entier d'un coup, pas les propriétés individuelles</p>
325 </div>
326 <div class="use-case">
327 <h4>shallowReactive()</h4>
328 <p>Structures de données plates avec beaucoup de propriétés au premier niveau</p>
329 </div>
330 <div class="use-case">
331 <h4>readonly()</h4>
332 <p>Configurations, constantes, données qui ne doivent pas être modifiées</p>
333 </div>
334 <div class="use-case">
335 <h4>triggerRef()</h4>
336 <p>Forcer manuellement la mise à jour après une modification de shallowRef</p>
337 </div>
338 </div>
339 </div>
340 </div>
341</template>
342
343<style scoped>
344.advanced-reactivity {
345 min-height: 100vh;
346 background: #0a0e27;
347 color: #e0e0e0;
348 padding: 2rem;
349 font-family: 'Courier New', monospace;
350}
351
352.header {
353 text-align: center;
354 margin-bottom: 2rem;
355 border-bottom: 2px solid #00ff88;
356 padding-bottom: 1rem;
357}
358
359.header h1 {
360 color: #00ff88;
361 font-size: 2rem;
362 margin: 0;
363 text-shadow: 0 0 10px #00ff88;
364}
365
366.subtitle {
367 color: #00b4d8;
368 margin: 0.5rem 0 0 0;
369}
370
371.demo-section {
372 background: #1a1f3a;
373 padding: 1.5rem;
374 border-radius: 8px;
375 border: 1px solid #00b4d8;
376 margin-bottom: 2rem;
377}
378
379.demo-section h2 {
380 color: #00ff88;
381 margin-top: 0;
382}
383
384.demo-grid {
385 display: grid;
386 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
387 gap: 1.5rem;
388 margin-bottom: 1.5rem;
389}
390
391.info-card, .data-card {
392 background: #0a0e27;
393 padding: 1.5rem;
394 border-radius: 6px;
395}
396
397.info-card h3, .data-card h3 {
398 color: #00b4d8;
399 margin-top: 0;
400}
401
402.info-card ul {
403 list-style: none;
404 padding: 0;
405}
406
407.info-card li {
408 margin: 0.8rem 0;
409 padding-left: 1.5rem;
410 position: relative;
411}
412
413.info-card code {
414 background: #1a1f3a;
415 padding: 0.2rem 0.5rem;
416 border-radius: 3px;
417 color: #00ff88;
418}
419
420.data-card pre {
421 background: #1a1f3a;
422 padding: 1rem;
423 border-radius: 4px;
424 overflow-x: auto;
425 font-size: 0.85rem;
426 color: #00ff88;
427}
428
429.controls {
430 display: flex;
431 flex-direction: column;
432 gap: 0.8rem;
433 margin: 1rem 0;
434}
435
436.btn {
437 padding: 0.8rem 1rem;
438 font-size: 0.9rem;
439 border: none;
440 border-radius: 6px;
441 cursor: pointer;
442 font-weight: bold;
443 transition: all 0.3s ease;
444 font-family: 'Courier New', monospace;
445 text-align: left;
446}
447
448.btn-success {
449 background: linear-gradient(135deg, #00ff88, #00dd77);
450 color: #0a0e27;
451}
452
453.btn-warning {
454 background: linear-gradient(135deg, #ffaa00, #ff8800);
455 color: #0a0e27;
456}
457
458.btn-danger {
459 background: linear-gradient(135deg, #ff4444, #cc0000);
460 color: white;
461}
462
463.btn-info {
464 background: linear-gradient(135deg, #00b4d8, #0088cc);
465 color: white;
466}
467
468.btn:hover {
469 transform: scale(1.02);
470 box-shadow: 0 0 15px currentColor;
471}
472
473.counter {
474 background: #1a1f3a;
475 padding: 0.8rem;
476 border-radius: 4px;
477 text-align: center;
478 color: #00ff88;
479 font-weight: bold;
480 margin-top: 1rem;
481}
482
483.status-display p {
484 margin: 0.5rem 0;
485 color: #ccc;
486}
487
488.phase-badge {
489 background: #00ff88;
490 color: #0a0e27;
491 padding: 0.3rem 0.8rem;
492 border-radius: 4px;
493 font-weight: bold;
494}
495
496.modules-grid {
497 display: grid;
498 gap: 1rem;
499 margin-top: 1rem;
500}
501
502.module-card {
503 background: #1a1f3a;
504 padding: 1rem;
505 border-radius: 4px;
506}
507
508.module-name {
509 color: #00b4d8;
510 font-weight: bold;
511 text-transform: uppercase;
512}
513
514.module-status {
515 color: #00ff88;
516 font-size: 0.9rem;
517 margin: 0.3rem 0;
518}
519
520.progress-bar {
521 height: 24px;
522 background: #0a0e27;
523 border-radius: 4px;
524 overflow: hidden;
525 margin-top: 0.5rem;
526}
527
528.progress-fill {
529 height: 100%;
530 background: linear-gradient(90deg, #00b4d8, #00ff88);
531 display: flex;
532 align-items: center;
533 justify-content: center;
534 color: #0a0e27;
535 font-weight: bold;
536 transition: width 0.3s ease;
537}
538
539.critical-params {
540 background: #1a1f3a;
541 padding: 1rem;
542 border-radius: 4px;
543 border-left: 3px solid #ff4444;
544}
545
546.param-item {
547 display: flex;
548 justify-content: space-between;
549 padding: 0.5rem 0;
550 border-bottom: 1px solid #333;
551}
552
553.param-item:last-child {
554 border-bottom: none;
555}
556
557.param-label {
558 color: #888;
559}
560
561.param-value {
562 color: #00ff88;
563 font-weight: bold;
564}
565
566.warning-box {
567 background: #2a1f1a;
568 border: 1px solid #ff8800;
569 padding: 1rem;
570 border-radius: 4px;
571 color: #ffaa00;
572 margin-top: 1rem;
573 text-align: center;
574}
575
576.code-example {
577 background: #0a0e27;
578 padding: 1rem;
579 border-radius: 6px;
580 border-left: 3px solid #00ff88;
581}
582
583.code-example h4 {
584 color: #00b4d8;
585 margin-top: 0;
586}
587
588.code-example pre {
589 color: #00ff88;
590 font-size: 0.85rem;
591 overflow-x: auto;
592 margin: 0;
593}
594
595.info-box {
596 background: #1a1f3a;
597 padding: 1.5rem;
598 border-radius: 8px;
599 border-left: 4px solid #00ff88;
600}
601
602.info-box h3 {
603 color: #00ff88;
604 margin-top: 0;
605}
606
607.use-cases {
608 display: grid;
609 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
610 gap: 1rem;
611 margin-top: 1rem;
612}
613
614.use-case {
615 background: #0a0e27;
616 padding: 1rem;
617 border-radius: 4px;
618}
619
620.use-case h4 {
621 color: #00b4d8;
622 margin-top: 0;
623}
624
625.use-case p {
626 color: #ccc;
627 margin: 0;
628 font-size: 0.9rem;
629}
630</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. Que renvoie la fonction isRef() ?
2. Que fait unref(value) ?
Voici 2 des 5 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 ref et reactive et utilise isRef() et isReactive() pour vérifier leurs types.
- Clique dans l'ordre
Range le code qui vérifie le type d'une valeur :
- Éditeur de code
Écris une fonction qui utilise unref() pour accepter à la fois des ref et des valeurs normales comme arguments.