Cours Vue.js · Module 3: Réactivité
toRef et toRefs - Extraction sûre des capteurs
Dans cette leçon6
Parfois, tu dois extraire des propriétés individuelles d'un objet capteur réactif tout en préservant la réactivité - comme isoler des lectures spécifiques d'un système de surveillance complexe.
Tu veux transmettre seulement la température à une fonction d'affichage, ou donner des noms plus courts aux champs dans le template, alors tu te tournes vers la déstructuration. Elle a l'air innocente, mais sur un objet réactif elle ne prend qu'une photo de la lecture à un instant donné. Cette leçon montre comment sortir une propriété avec le câble qui continue de transmettre les changements.
Le problème de la déstructuration
Regarde ce qui arrive aux valeurs sorties d'un objet reactive() quand l'objet change ensuite :
1import { reactive } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500
6})
7
8// MAUVAIS - perd la réactivité !
9const { name, temperature } = reactor
10console.log(name) // 'Plasma Core Alpha' - mais pas réactif
11
12reactor.name = 'Plasma Core Beta'
13console.log(name) // Toujours 'Plasma Core Alpha' !La variable name a reçu une simple chaîne copiée au moment de la déstructuration. L'objet reactor a bien changé, mais la copie n'en sait rien et ne rafraîchira aucune vue. Il en va de même quand tu passes reactor.temperature à une fonction : elle reçoit un nombre, pas une connexion au réacteur.
toRef() - Un seul capteur
toRef(objet, 'clé') crée un ref lié à une seule propriété. La connexion fonctionne dans les deux sens :
1import { reactive, toRef } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500
6})
7
8// toRef crée une référence réactive vers une lecture
9const nameRef = toRef(reactor, 'name')
10
11console.log(nameRef.value) // 'Plasma Core Alpha'
12
13reactor.name = 'Plasma Core Beta'
14console.log(nameRef.value) // 'Plasma Core Beta' - synchronisé !
15
16nameRef.value = 'Plasma Core Gamma'
17console.log(reactor.name) // 'Plasma Core Gamma' - bidirectionnel !nameRef ne garde pas sa propre copie : il lit et écrit reactor.name. C'est différent de ref(reactor.name), qui recevrait une simple chaîne et ne serait synchronisé avec rien.
toRefs() - Tous les capteurs
toRefs() fait la même chose pour toutes les propriétés d'un coup et renvoie un objet ordinaire rempli de refs, que l'on peut déstructurer sans risque :
1import { reactive, toRefs } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500,
6 status: 'ACTIVE'
7})
8
9// toRefs convertit toutes les propriétés
10const { name, temperature, status } = toRefs(reactor)
11
12// Chacune est maintenant un ref avec .value
13console.log(name.value) // 'Plasma Core Alpha'
14console.log(temperature.value) // 2500
15
16// Les changements sont synchronisés
17reactor.temperature = 2600
18console.log(temperature.value) // 2600toRefs ne crée des refs que pour les propriétés qui existent au moment de l'appel. Un champ ajouté plus tard n'aura pas de ref - pour un champ optionnel, utilise toRef. Le même mécanisme sert quand une fonction renvoie un état : renvoyer toRefs(state) permet à l'appelant de déstructurer le résultat sans perdre la réactivité.
Utilisation dans les composants
Les archives de la station conservent des copies holographiques d'œuvres d'art terriennes. Le formulaire d'édition utilise v-model, la directive de liaison bidirectionnelle : le texte saisi va dans les données, et un changement des données revient dans le champ. Tu la découvriras en détail dans le module 5.
1<template>
2 <div>
3 <input v-model="title" />
4 <p>Année : {{ year }}</p>
5 </div>
6</template>
7
8<script setup>
9import { reactive, toRefs } from 'vue'
10
11const artwork = reactive({
12 title: 'Mona Lisa',
13 year: 1503
14})
15
16// Extraire pour utiliser dans le template
17const { title, year } = toRefs(artwork)
18</script>Taper dans le champ modifie artwork.title par l'intermédiaire du ref title. Dans le template, tu pourrais tout aussi bien écrire artwork.title - toRefs est surtout utile quand tu renvoies un état depuis une fonction ou que tu le transmets plus loin.
toRef avec valeur par défaut
Le troisième argument de toRef est une valeur par défaut, utilisée quand la propriété vaut undefined :
1import { reactive, toRef } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha'
5 // pas de 'maxTemperature'
6})
7
8// Avec valeur par défaut (Vue 3.2.25+)
9const maxTemperature = toRef(reactor, 'maxTemperature', 5000)
10console.log(maxTemperature.value) // 5000L'objet reactor n'a pas gagné de nouveau champ - la valeur par défaut ne vit que dans le ref. C'est pratique pour les lots de télémétrie dont certains champs sont vides.
Exemple pratique - Props
Les props sont les données qu'un composant reçoit de son parent. La macro defineProps les déclare sans import, et un type écrit entre chevrons exige lang="ts" :
1<!-- SensorDisplay.vue -->
2<script setup lang="ts">
3import { toRefs } from 'vue'
4
5const props = defineProps<{
6 sensorName: string
7 reading: number
8}>()
9
10// Extraire les props tout en préservant la réactivité
11const { sensorName, reading } = toRefs(props)
12
13// Tu peux maintenant les utiliser dans watch, computed, etc.
14</script>L'objet props est réactif, donc toRefs te donne des refs qui suivent le parent. Depuis Vue 3.5, même les variables déstructurées directement depuis defineProps() restent réactives, car le compilateur les transforme en props.sensorName. Quand tu passes une telle prop à une autre fonction, il te faut cependant toujours un ref ou un getter comme () => props.reading. Tu découvriras les composants et les props dans le module 6.
Mon choix : toRef pour un seul champ, toRefs pour un objet entier, et la simple notation pointée dans un template simple. Dans la prochaine leçon, tu découvriras des outils qui te diront si une valeur est un ref, un objet réactif ou une simple copie.
Retiens : la déstructuration prend une photo de la lecture, tandis que toRef et toRefs branchent un câble qui transmet les changements dans les deux sens.
Code de cette leçon : App.vue
1<script setup>
2import { ref, reactive, isRef, isReactive, unref } from 'vue'
3
4// Diagnostic système - isRef(), isReactive(), unref()
5// Vérifier les types de réactivité au NOVA LAB
6
7// Différents types de données
8const systemName = ref('Plasma Core System')
9const isPowered = ref(true)
10const powerLevel = ref(75)
11
12const systemConfig = reactive({
13 id: 'SYS-001',
14 type: 'REACTOR',
15 online: true,
16 sensors: {
17 temperature: 850,
18 pressure: 120
19 }
20})
21
22const normalString = 'This is not reactive'
23const normalNumber = 42
24const normalObject = { test: 'value' }
25
26// Fonction de diagnostic
27function diagnoseValue(value, name) {
28 return {
29 name,
30 isRef: isRef(value),
31 isReactive: isReactive(value),
32 rawValue: unref(value), // unref() renvoie la valeur sans .value
33 type: typeof value
34 }
35}
36
37// Résultats du diagnostic
38const diagnosticResults = ref([])
39
40function runDiagnostics() {
41 diagnosticResults.value = [
42 diagnoseValue(systemName, 'systemName'),
43 diagnoseValue(isPowered, 'isPowered'),
44 diagnoseValue(powerLevel, 'powerLevel'),
45 diagnoseValue(systemConfig, 'systemConfig'),
46 diagnoseValue(systemConfig.sensors, 'systemConfig.sensors'),
47 diagnoseValue(normalString, 'normalString'),
48 diagnoseValue(normalNumber, 'normalNumber'),
49 diagnoseValue(normalObject, 'normalObject')
50 ]
51}
52
53// Exemple d'utilisation d'unref()
54function getValue(maybeRef) {
55 // unref() extrait la valeur d'un ref en toute sécurité, ou renvoie la valeur normale
56 return unref(maybeRef)
57}
58
59// Fonction universelle acceptant un ref ou une valeur
60function updatePowerLevel(newLevel) {
61 // On vérifie si c'est un ref
62 if (isRef(powerLevel)) {
63 powerLevel.value = newLevel
64 } else {
65 console.log('powerLevel is not a ref!')
66 }
67}
68
69// Fonction universelle pour les objets
70function updateSystemStatus(system, status) {
71 if (isReactive(system)) {
72 system.online = status
73 } else {
74 console.log('System is not reactive!')
75 }
76}
77
78// Exemples pratiques
79const mixedData = ref([
80 { name: 'Sensor A', value: ref(100) },
81 { name: 'Sensor B', value: 200 },
82 { name: 'Sensor C', value: ref(300) }
83])
84
85function processValue(item) {
86 // On utilise unref() pour récupérer la valeur en toute sécurité
87 const value = unref(item.value)
88 return value * 1.1
89}
90
91// Lancer le diagnostic au chargement
92runDiagnostics()
93</script>
94
95<template>
96 <div class="diagnostics">
97 <div class="header">
98 <h1>DIAGNOSTIC SYSTÈME - NOVA LAB</h1>
99 <p class="subtitle">isRef(), isReactive(), unref() - Vérifier la réactivité</p>
100 </div>
101
102 <div class="system-panel">
103 <h2>Statut du système</h2>
104 <div class="status-grid">
105 <div class="status-item">
106 <div class="label">Nom du système</div>
107 <div class="value">{{ systemName }}</div>
108 <div class="type-badge ref">REF</div>
109 </div>
110
111 <div class="status-item">
112 <div class="label">État de l'alimentation</div>
113 <div class="value">{{ isPowered ? 'ONLINE' : 'OFFLINE' }}</div>
114 <div class="type-badge ref">REF</div>
115 </div>
116
117 <div class="status-item">
118 <div class="label">Niveau de puissance</div>
119 <div class="value">{{ powerLevel }}%</div>
120 <div class="type-badge ref">REF</div>
121 </div>
122
123 <div class="status-item">
124 <div class="label">ID du système</div>
125 <div class="value">{{ systemConfig.id }}</div>
126 <div class="type-badge reactive">REACTIVE</div>
127 </div>
128
129 <div class="status-item">
130 <div class="label">Température</div>
131 <div class="value">{{ systemConfig.sensors.temperature }}°C</div>
132 <div class="type-badge reactive">REACTIVE</div>
133 </div>
134
135 <div class="status-item">
136 <div class="label">Pression</div>
137 <div class="value">{{ systemConfig.sensors.pressure }} bar</div>
138 <div class="type-badge reactive">REACTIVE</div>
139 </div>
140 </div>
141 </div>
142
143 <div class="diagnostics-panel">
144 <div class="panel-header">
145 <h2>Résultats du diagnostic de réactivité</h2>
146 <button @click="runDiagnostics" class="btn btn-scan">
147 Relancer
148 </button>
149 </div>
150
151 <div class="results-table">
152 <div class="table-header">
153 <div>Nom de la variable</div>
154 <div>isRef()</div>
155 <div>isReactive()</div>
156 <div>Valeur (unref)</div>
157 <div>Type</div>
158 </div>
159 <div
160 v-for="result in diagnosticResults"
161 :key="result.name"
162 class="table-row"
163 >
164 <div class="cell-name">{{ result.name }}</div>
165 <div class="cell-check">
166 <span v-if="result.isRef" class="check-yes">TRUE</span>
167 <span v-else class="check-no">FALSE</span>
168 </div>
169 <div class="cell-check">
170 <span v-if="result.isReactive" class="check-yes">TRUE</span>
171 <span v-else class="check-no">FALSE</span>
172 </div>
173 <div class="cell-value">
174 {{ typeof result.rawValue === 'object' ? JSON.stringify(result.rawValue) : result.rawValue }}
175 </div>
176 <div class="cell-type">{{ result.type }}</div>
177 </div>
178 </div>
179 </div>
180
181 <div class="examples-section">
182 <h2>Exemples pratiques</h2>
183
184 <div class="example-grid">
185 <div class="example-card">
186 <h3>unref() - Récupération universelle de valeur</h3>
187 <div class="code-box">
188 <pre>function getValue(maybeRef) {
189 return unref(maybeRef)
190}
191
192const refValue = ref(100)
193const normalValue = 200
194
195getValue(refValue) // 100
196getValue(normalValue) // 200</pre>
197 </div>
198 <div class="demo">
199 <p>getValue(powerLevel): <strong>{{ getValue(powerLevel) }}</strong></p>
200 <p>getValue(normalNumber): <strong>{{ getValue(normalNumber) }}</strong></p>
201 </div>
202 </div>
203
204 <div class="example-card">
205 <h3>isRef() - Vérifier si une valeur est un ref</h3>
206 <div class="code-box">
207 <pre>if (isRef(value)) {
208 value.value = newValue
209} else {
210 // gérer une valeur normale
211}</pre>
212 </div>
213 <div class="demo">
214 <p>isRef(powerLevel) : <strong class="check-yes">{{ isRef(powerLevel) }}</strong></p>
215 <p>isRef(normalNumber) : <strong class="check-no">{{ isRef(normalNumber) }}</strong></p>
216 <button @click="updatePowerLevel(90)" class="btn btn-small">
217 Régler la puissance à 90%
218 </button>
219 </div>
220 </div>
221
222 <div class="example-card">
223 <h3>isReactive() - Vérifier si un objet est reactive</h3>
224 <div class="code-box">
225 <pre>if (isReactive(obj)) {
226 obj.property = newValue
227} else {
228 // gérer un objet normal
229}</pre>
230 </div>
231 <div class="demo">
232 <p>isReactive(systemConfig) : <strong class="check-yes">{{ isReactive(systemConfig) }}</strong></p>
233 <p>isReactive(normalObject) : <strong class="check-no">{{ isReactive(normalObject) }}</strong></p>
234 <button @click="updateSystemStatus(systemConfig, !systemConfig.online)" class="btn btn-small">
235 Basculer le statut du système
236 </button>
237 </div>
238 </div>
239
240 <div class="example-card">
241 <h3>Données mixtes - cas pratique</h3>
242 <div class="code-box">
243 <pre>// Certaines valeurs sont des ref, d'autres non
244const sensors = [
245 { name: 'A', value: ref(100) },
246 { name: 'B', value: 200 }
247]
248
249// unref() gère les deux
250sensors.forEach(s => {
251 const val = unref(s.value)
252})</pre>
253 </div>
254 <div class="demo">
255 <div v-for="item in mixedData" :key="item.name" class="sensor-item">
256 <span>{{ item.name }}:</span>
257 <span class="sensor-value">{{ processValue(item) }}</span>
258 <span v-if="isRef(item.value)" class="type-badge ref">REF</span>
259 <span v-else class="type-badge normal">NORMAL</span>
260 </div>
261 </div>
262 </div>
263 </div>
264 </div>
265
266 <div class="info-box">
267 <h3>Quand utiliser ces fonctions ?</h3>
268 <ul>
269 <li>
270 <strong>isRef(value) :</strong> vérifie si une valeur est un ref. Utile dans les fonctions universelles.
271 </li>
272 <li>
273 <strong>isReactive(value) :</strong> vérifie si un objet est reactive. Aide au débogage.
274 </li>
275 <li>
276 <strong>unref(value) :</strong> renvoie la valeur d'un ref ou la valeur originale. Façon sûre d'y accéder.
277 </li>
278 <li>
279 <strong>Utilisation pratique :</strong> créer des fonctions compatibles à la fois avec les ref et les valeurs normales.
280 </li>
281 </ul>
282 </div>
283 </div>
284</template>
285
286<style scoped>
287.diagnostics {
288 min-height: 100vh;
289 background: #0a0e27;
290 color: #e0e0e0;
291 padding: 2rem;
292 font-family: 'Courier New', monospace;
293}
294
295.header {
296 text-align: center;
297 margin-bottom: 2rem;
298 border-bottom: 2px solid #00ff88;
299 padding-bottom: 1rem;
300}
301
302.header h1 {
303 color: #00ff88;
304 font-size: 2rem;
305 margin: 0;
306 text-shadow: 0 0 10px #00ff88;
307}
308
309.subtitle {
310 color: #00b4d8;
311 margin: 0.5rem 0 0 0;
312}
313
314.system-panel {
315 background: #1a1f3a;
316 padding: 1.5rem;
317 border-radius: 8px;
318 border: 1px solid #00b4d8;
319 margin-bottom: 2rem;
320}
321
322.system-panel h2 {
323 color: #00b4d8;
324 margin-top: 0;
325}
326
327.status-grid {
328 display: grid;
329 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
330 gap: 1rem;
331}
332
333.status-item {
334 background: #0a0e27;
335 padding: 1rem;
336 border-radius: 6px;
337 position: relative;
338}
339
340.label {
341 font-size: 0.85rem;
342 color: #888;
343 margin-bottom: 0.5rem;
344}
345
346.value {
347 font-size: 1.3rem;
348 color: #00ff88;
349 font-weight: bold;
350}
351
352.type-badge {
353 position: absolute;
354 top: 0.5rem;
355 right: 0.5rem;
356 padding: 0.2rem 0.5rem;
357 border-radius: 4px;
358 font-size: 0.7rem;
359 font-weight: bold;
360}
361
362.type-badge.ref {
363 background: #00b4d8;
364 color: #0a0e27;
365}
366
367.type-badge.reactive {
368 background: #00ff88;
369 color: #0a0e27;
370}
371
372.type-badge.normal {
373 background: #666;
374 color: white;
375}
376
377.diagnostics-panel {
378 background: #1a1f3a;
379 padding: 1.5rem;
380 border-radius: 8px;
381 border: 1px solid #00ff88;
382 margin-bottom: 2rem;
383}
384
385.panel-header {
386 display: flex;
387 justify-content: space-between;
388 align-items: center;
389 margin-bottom: 1.5rem;
390}
391
392.panel-header h2 {
393 color: #00ff88;
394 margin: 0;
395}
396
397.btn {
398 padding: 0.6rem 1.2rem;
399 font-size: 0.9rem;
400 border: none;
401 border-radius: 6px;
402 cursor: pointer;
403 font-weight: bold;
404 transition: all 0.3s ease;
405 font-family: 'Courier New', monospace;
406}
407
408.btn-scan {
409 background: linear-gradient(135deg, #00ff88, #00b4d8);
410 color: #0a0e27;
411}
412
413.btn-scan:hover {
414 transform: scale(1.05);
415 box-shadow: 0 0 20px #00ff88;
416}
417
418.btn-small {
419 padding: 0.5rem 1rem;
420 font-size: 0.85rem;
421}
422
423.results-table {
424 background: #0a0e27;
425 border-radius: 6px;
426 overflow: hidden;
427}
428
429.table-header, .table-row {
430 display: grid;
431 grid-template-columns: 2fr 1fr 1fr 2fr 1fr;
432 gap: 1rem;
433 padding: 1rem;
434 align-items: center;
435}
436
437.table-header {
438 background: #00ff8820;
439 color: #00ff88;
440 font-weight: bold;
441 border-bottom: 2px solid #00ff88;
442}
443
444.table-row {
445 border-bottom: 1px solid #333;
446}
447
448.table-row:hover {
449 background: #1a1f3a;
450}
451
452.cell-name {
453 color: #00b4d8;
454 font-family: monospace;
455}
456
457.cell-check {
458 text-align: center;
459}
460
461.check-yes {
462 color: #00ff88;
463}
464
465.check-no {
466 color: #666;
467}
468
469.cell-value {
470 color: #ccc;
471 font-size: 0.9rem;
472 overflow: hidden;
473 text-overflow: ellipsis;
474}
475
476.cell-type {
477 color: #888;
478 font-style: italic;
479}
480
481.examples-section {
482 margin-bottom: 2rem;
483}
484
485.examples-section h2 {
486 color: #00ff88;
487 margin-bottom: 1.5rem;
488}
489
490.example-grid {
491 display: grid;
492 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
493 gap: 1.5rem;
494}
495
496.example-card {
497 background: #1a1f3a;
498 padding: 1.5rem;
499 border-radius: 8px;
500 border: 1px solid #333;
501}
502
503.example-card h3 {
504 color: #00b4d8;
505 margin-top: 0;
506 font-size: 1.1rem;
507}
508
509.code-box {
510 background: #0a0e27;
511 padding: 1rem;
512 border-radius: 4px;
513 border-left: 3px solid #00ff88;
514 margin: 1rem 0;
515}
516
517.code-box pre {
518 margin: 0;
519 color: #00ff88;
520 font-size: 0.85rem;
521 overflow-x: auto;
522}
523
524.demo {
525 background: #0a0e27;
526 padding: 1rem;
527 border-radius: 4px;
528}
529
530.demo p {
531 margin: 0.5rem 0;
532 color: #ccc;
533}
534
535.demo strong {
536 color: #00ff88;
537}
538
539.sensor-item {
540 display: flex;
541 justify-content: space-between;
542 align-items: center;
543 padding: 0.5rem;
544 margin: 0.3rem 0;
545 background: #1a1f3a;
546 border-radius: 4px;
547}
548
549.sensor-value {
550 color: #00ff88;
551 font-weight: bold;
552}
553
554.info-box {
555 background: #1a1f3a;
556 padding: 1.5rem;
557 border-radius: 8px;
558 border-left: 4px solid #00ff88;
559}
560
561.info-box h3 {
562 color: #00ff88;
563 margin-top: 0;
564}
565
566.info-box ul {
567 color: #ccc;
568 line-height: 1.8;
569}
570
571.info-box strong {
572 color: #00b4d8;
573}
574</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 se passe-t-il quand on déstructure un objet reactive ?
2. Que fait toRef() ?
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 un objet réactif et utilise toRef() pour créer un ref lié à une seule propriété.
- Clique dans l'ordre
Range dans le bon ordre les éléments de la syntaxe toRef().
- Éditeur de code
Crée un objet réactif et utilise toRefs() pour une déstructuration sûre qui préserve la réactivité.
- Tri vertical
Range dans le bon ordre les étapes d'une déstructuration sûre avec toRefs().
- Tri horizontal
Range dans le bon ordre les éléments d'une déstructuration avec toRefs.