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

toRef et toRefs - Extraction sûre des capteurs

5 min de lecture
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) // 2600

toRefs 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) // 5000

L'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. 1. Que se passe-t-il quand on déstructure un objet reactive ?

  2. 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.