Cours Vue.js · Module 3: Réactivité
reactive() - Les systèmes réactifs
Dans cette leçon4
Quand tu travailles avec des objets système complexes, reactive() est souvent plus pratique que ref() - c'est comme un panneau de contrôle complet du réacteur plutôt que des jauges individuelles.
Le réacteur a un nom, un statut, une température et deux lectures : la puissance et le rayonnement. Cinq refs séparés, c'est cinq déclarations et .value à chaque ligne du script, alors que toutes ces données décrivent un seul appareil. reactive() te permet de les enfermer dans un seul objet, réactif dans son ensemble.
Les bases de reactive()
reactive() prend un objet et renvoie son Proxy - un mécanisme intégré à JavaScript qui intercepte chaque lecture et chaque écriture d'une propriété. Grâce à lui, tu n'as pas besoin de .value, et les objets imbriqués deviennent eux aussi réactifs :
1import { reactive } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 status: 'ACTIVE',
6 temperature: 2500,
7 readings: {
8 power: 850,
9 radiation: 12
10 }
11})
12
13// Accès sans .value !
14console.log(reactor.name) // 'Plasma Core Alpha'
15reactor.temperature = 2600
16
17// Les objets imbriqués sont aussi réactifs
18reactor.readings.power = 900 // Cela déclenchera aussi une mise à jour du panneauLe Proxy agit comme un répartiteur entre toi et les données : il signale chaque changement au système, qui rafraîchit les vues dépendantes. Remarque ce qui n'a pas changé - seul l'objet renvoyé par reactive() est réactif, pas l'original que tu lui as passé :
1const raw = { status: 'STANDBY' }
2const panel = reactive(raw)
3
4console.log(panel === raw) // false
5console.log(reactive(raw) === panel) // trueLe Proxy est un autre objet que l'original, et les changements écrits directement dans raw ne rafraîchiront pas la vue. Un nouvel appel à reactive() sur le même objet renvoie le même Proxy, alors travaille toujours avec la version réactive.
reactive() vs ref()
Les deux outils apportent la réactivité, mais ils diffèrent dans l'usage quotidien :
| Caractéristique | ref() | reactive() |
|---|---|---|
| Pour les primitives | Oui | Non |
| Pour les objets | Oui (.value) | Oui (directement) |
| Déstructuration | Sûre (tu passes tout le conteneur) | Perd la réactivité |
| Réaffectation | Oui (.value = newObj) | Non (uniquement les propriétés) |
Tu peux passer un ref à une fonction ou le sortir d'un objet, il reste réactif, car c'est tout le conteneur qui voyage. Avec reactive(), en revanche, tu extrais des valeurs nues. La documentation de Vue recommande ref() comme API principale justement à cause de ces limites.
Exemple pratique
Voici un panneau de réacteur construit sur un seul objet. Le template lit aussi les lectures imbriquées :
1<template>
2 <div class="reactor-panel" style="background: #0a0e27; color: #00ff88; border: 1px solid #00b4d8;">
3 <h2>{{ reactor.name }}</h2>
4 <p>Statut : {{ reactor.status }}</p>
5 <p>Température : {{ reactor.temperature }} K</p>
6 <p>Lectures : {{ reactor.readings.power }} MW | {{ reactor.readings.radiation }} Sv/h</p>
7
8 <button @click="updateTemperature">Augmenter la température</button>
9 </div>
10</template>Il n'y a pas un seul .value dans le template, car reactor est un objet réactif, pas un ref. Le script crée cet objet et une fonction qui modifie une propriété :
1<script setup>
2import { reactive } from 'vue'
3
4const reactor = reactive({
5 name: 'Plasma Core Alpha',
6 status: 'ACTIVE',
7 temperature: 2500,
8 readings: {
9 power: 850,
10 radiation: 12
11 }
12})
13
14function updateTemperature() {
15 reactor.temperature += 100 // Modification directe
16}
17</script>Chaque clic augmente la température de 100 K, et le panneau affiche la nouvelle valeur. La variable reactor pointe tout le temps vers le même Proxy - seules ses propriétés changent.
Limites de reactive()
Première limite : reactive() ne fonctionne qu'avec les objets, les tableaux et les collections Map et Set. Pour un nombre, Vue affiche un avertissement et le renvoie tel quel. La deuxième concerne le remplacement de l'objet entier :
1import { reactive } from 'vue'
2
3const reactor = reactive({ name: 'Plasma Core Alpha' })
4
5// MAUVAIS - remplacer l'objet entier
6reactor = { name: 'Plasma Core Beta' } // Cela ne marchera pas !
7
8// BON - modifier les propriétés
9reactor.name = 'Plasma Core Beta'Avec const, JavaScript lui-même lève une TypeError (affectation à une constante). Avec let, la variable pointerait vers un nouvel objet ordinaire tandis que le template resterait lié à l'ancien Proxy, donc la connexion serait rompue. C'est pourquoi tu modifies les propriétés, pas l'objet entier. Quand de nouvelles données arrivent d'un bloc, par exemple de la télémétrie, recopie-les avec Object.assign(reactor, newData) - cela reste une modification des propriétés du même Proxy. La troisième limite est la déstructuration :
1// MAUVAIS - la déstructuration perd la réactivité
2const { name } = reactor // name n'est pas réactif !
3
4// BON - utilise toRefs
5import { toRefs } from 'vue'
6const { name } = toRefs(reactor) // name.value est réactifLa déstructuration copie la valeur actuelle, alors que toRefs crée des refs liés aux propriétés - nous le verrons en détail dans la leçon sur toRef et toRefs. Ma pratique au NOVA LAB : ref() par défaut, et reactive() pour un groupe cohérent de champs, comme un formulaire de configuration. Dans la prochaine leçon, tu vérifieras jusqu'où va cette réactivité et quand il vaut la peine de la rendre superficielle.
Retiens : reactive() est un panneau de contrôle derrière une vitre Proxy - tu tournes les boutons, mais tu ne remplaces jamais tout le panneau.
Code de cette leçon : App.vue
1<script setup>
2import { ref, reactive } from 'vue'
3
4// Comparaison ref() vs reactive()
5// Gestion des réservoirs d'oxygène de la station NOVA LAB
6
7// ref() - valeurs individuelles
8const mainTankLevel = ref(100)
9const backupTankLevel = ref(75)
10const oxygenFlow = ref(2.5)
11
12// reactive() - objet complexe
13const oxygenSystem = reactive({
14 mainTank: {
15 level: 100,
16 capacity: 1000,
17 pressure: 150,
18 status: 'OPTIMAL'
19 },
20 backupTank: {
21 level: 75,
22 capacity: 500,
23 pressure: 120,
24 status: 'STANDBY'
25 },
26 flow: {
27 rate: 2.5,
28 direction: 'OUT',
29 consumers: 12
30 }
31})
32
33// Simulation de la consommation d'oxygène
34let simulationInterval = null
35
36function startSimulation() {
37 if (simulationInterval) return
38
39 simulationInterval = setInterval(() => {
40 // Avec ref - on doit utiliser .value
41 if (mainTankLevel.value > 0) {
42 mainTankLevel.value -= 0.5
43 }
44
45 // Avec reactive - accès direct aux propriétés
46 if (oxygenSystem.mainTank.level > 0) {
47 oxygenSystem.mainTank.level -= 0.5
48
49 if (oxygenSystem.mainTank.level < 30) {
50 oxygenSystem.mainTank.status = 'WARNING'
51 }
52 if (oxygenSystem.mainTank.level < 10) {
53 oxygenSystem.mainTank.status = 'CRITICAL'
54 }
55 }
56 }, 100)
57}
58
59function stopSimulation() {
60 if (simulationInterval) {
61 clearInterval(simulationInterval)
62 simulationInterval = null
63 }
64}
65
66function refill() {
67 // ref - on utilise .value
68 mainTankLevel.value = 100
69 backupTankLevel.value = 75
70
71 // reactive - accès direct
72 oxygenSystem.mainTank.level = 100
73 oxygenSystem.mainTank.status = 'OPTIMAL'
74 oxygenSystem.backupTank.level = 75
75}
76
77// Exemple : on ne peut pas remplacer l'objet reactive entier
78function wrongWayToUpdate() {
79 // CECI NE MARCHERA PAS - perd la réactivité
80 // oxygenSystem = { ...newData }
81
82 // BONNE MÉTHODE - mettre à jour les propriétés
83 oxygenSystem.mainTank.level = 100
84 oxygenSystem.mainTank.status = 'OPTIMAL'
85}
86</script>
87
88<template>
89 <div class="oxygen-control">
90 <div class="header">
91 <h1>SYSTÈME D'OXYGÈNE - NOVA LAB</h1>
92 <p class="subtitle">ref() vs reactive() - Comparaison</p>
93 </div>
94
95 <div class="comparison-grid">
96 <!-- Section ref() -->
97 <div class="section">
98 <h2 class="section-title">UTILISER ref()</h2>
99 <div class="card">
100 <div class="tank-visual">
101 <div class="tank-fill" :style="{ height: mainTankLevel + '%' }">
102 <span class="tank-label">{{ mainTankLevel.toFixed(1) }}%</span>
103 </div>
104 </div>
105 <div class="tank-info">
106 <p><strong>Réservoir principal :</strong> {{ mainTankLevel.toFixed(1) }}%</p>
107 <p><strong>Réservoir de secours :</strong> {{ backupTankLevel.toFixed(1) }}%</p>
108 <p><strong>Débit :</strong> {{ oxygenFlow }} L/min</p>
109 </div>
110 <div class="code-example">
111 <pre>const level = ref(100)
112// Accès : level.value
113level.value = 50</pre>
114 </div>
115 </div>
116 </div>
117
118 <!-- Section reactive() -->
119 <div class="section">
120 <h2 class="section-title">UTILISER reactive()</h2>
121 <div class="card">
122 <div class="tank-visual">
123 <div
124 class="tank-fill"
125 :style="{ height: oxygenSystem.mainTank.level + '%' }"
126 :class="oxygenSystem.mainTank.status.toLowerCase()"
127 >
128 <span class="tank-label">{{ oxygenSystem.mainTank.level.toFixed(1) }}%</span>
129 </div>
130 </div>
131 <div class="tank-info">
132 <p>
133 <strong>Status:</strong>
134 <span :class="'status-' + oxygenSystem.mainTank.status.toLowerCase()">
135 {{ oxygenSystem.mainTank.status }}
136 </span>
137 </p>
138 <p><strong>Capacité :</strong> {{ oxygenSystem.mainTank.capacity }} L</p>
139 <p><strong>Pression :</strong> {{ oxygenSystem.mainTank.pressure }} bar</p>
140 <p><strong>Débit :</strong> {{ oxygenSystem.flow.rate }} L/min</p>
141 <p><strong>Consommateurs :</strong> {{ oxygenSystem.flow.consumers }}</p>
142 </div>
143 <div class="code-example">
144 <pre>const system = reactive({
145 mainTank: { level: 100 }
146})
147// Accès : system.mainTank.level
148system.mainTank.level = 50</pre>
149 </div>
150 </div>
151 </div>
152 </div>
153
154 <div class="controls">
155 <button @click="startSimulation" class="btn btn-start">
156 ▶DÉMARRER LA SIMULATION
157 </button>
158 <button @click="stopSimulation" class="btn btn-stop">
159 ⏸ARRÊTER
160 </button>
161 <button @click="refill" class="btn btn-refill">
162 REMPLIR
163 </button>
164 </div>
165
166 <div class="info-box">
167 <h3>Quand utiliser ref() vs reactive() ?</h3>
168 <div class="comparison-table">
169 <div class="comparison-col">
170 <h4>ref()</h4>
171 <ul>
172 <li>Valeurs primitives (string, number, boolean)</li>
173 <li>Quand tu veux remplacer la valeur entière</li>
174 <li>Plus facile à déboguer</li>
175 <li>Nécessite .value dans le script</li>
176 </ul>
177 </div>
178 <div class="comparison-col">
179 <h4>reactive()</h4>
180 <ul>
181 <li>Objets et tableaux</li>
182 <li>Regrouper des données liées</li>
183 <li>Ne nécessite pas .value</li>
184 <li>Impossible de remplacer l'objet entier</li>
185 </ul>
186 </div>
187 </div>
188 </div>
189 </div>
190</template>
191
192<style scoped>
193.oxygen-control {
194 min-height: 100vh;
195 background: #0a0e27;
196 color: #e0e0e0;
197 padding: 2rem;
198 font-family: 'Courier New', monospace;
199}
200
201.header {
202 text-align: center;
203 margin-bottom: 2rem;
204 border-bottom: 2px solid #00ff88;
205 padding-bottom: 1rem;
206}
207
208.header h1 {
209 color: #00ff88;
210 font-size: 2rem;
211 margin: 0;
212 text-shadow: 0 0 10px #00ff88;
213}
214
215.subtitle {
216 color: #00b4d8;
217 margin: 0.5rem 0 0 0;
218}
219
220.comparison-grid {
221 display: grid;
222 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
223 gap: 2rem;
224 margin-bottom: 2rem;
225}
226
227.section-title {
228 color: #00b4d8;
229 font-size: 1.3rem;
230 margin-bottom: 1rem;
231 text-align: center;
232}
233
234.card {
235 background: #1a1f3a;
236 padding: 1.5rem;
237 border-radius: 8px;
238 border: 1px solid #00b4d8;
239}
240
241.tank-visual {
242 width: 100%;
243 height: 200px;
244 background: #0a0e27;
245 border: 2px solid #00b4d8;
246 border-radius: 8px;
247 position: relative;
248 overflow: hidden;
249 margin-bottom: 1rem;
250}
251
252.tank-fill {
253 position: absolute;
254 bottom: 0;
255 width: 100%;
256 background: linear-gradient(180deg, #00ff88, #00b4d8);
257 transition: height 0.3s ease;
258 display: flex;
259 align-items: center;
260 justify-content: center;
261}
262
263.tank-fill.warning {
264 background: linear-gradient(180deg, #ffaa00, #ff8800);
265}
266
267.tank-fill.critical {
268 background: linear-gradient(180deg, #ff4444, #cc0000);
269}
270
271.tank-label {
272 color: #0a0e27;
273 font-weight: bold;
274 font-size: 1.5rem;
275 text-shadow: 0 0 5px rgba(255,255,255,0.5);
276}
277
278.tank-info {
279 margin-bottom: 1rem;
280}
281
282.tank-info p {
283 margin: 0.5rem 0;
284 color: #ccc;
285}
286
287.status-optimal {
288 color: #00ff88;
289}
290
291.status-warning {
292 color: #ffaa00;
293}
294
295.status-critical {
296 color: #ff4444;
297 animation: blink 1s infinite;
298}
299
300@keyframes blink {
301 0%, 50%, 100% { opacity: 1; }
302 25%, 75% { opacity: 0.5; }
303}
304
305.code-example {
306 background: #0a0e27;
307 padding: 1rem;
308 border-radius: 4px;
309 border-left: 3px solid #00ff88;
310}
311
312.code-example pre {
313 margin: 0;
314 color: #00ff88;
315 font-size: 0.85rem;
316 overflow-x: auto;
317}
318
319.controls {
320 display: flex;
321 gap: 1rem;
322 margin-bottom: 2rem;
323 justify-content: center;
324 flex-wrap: wrap;
325}
326
327.btn {
328 padding: 1rem 2rem;
329 font-size: 1rem;
330 border: none;
331 border-radius: 8px;
332 cursor: pointer;
333 font-weight: bold;
334 transition: all 0.3s ease;
335 font-family: 'Courier New', monospace;
336}
337
338.btn-start {
339 background: linear-gradient(135deg, #00ff88, #00b4d8);
340 color: #0a0e27;
341}
342
343.btn-stop {
344 background: linear-gradient(135deg, #ffaa00, #ff8800);
345 color: #0a0e27;
346}
347
348.btn-refill {
349 background: linear-gradient(135deg, #00b4d8, #0088cc);
350 color: white;
351}
352
353.btn:hover {
354 transform: scale(1.05);
355 box-shadow: 0 0 20px currentColor;
356}
357
358.info-box {
359 background: #1a1f3a;
360 padding: 1.5rem;
361 border-radius: 8px;
362 border-left: 4px solid #00ff88;
363}
364
365.info-box h3 {
366 color: #00ff88;
367 margin-top: 0;
368}
369
370.comparison-table {
371 display: grid;
372 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
373 gap: 1.5rem;
374 margin-top: 1rem;
375}
376
377.comparison-col h4 {
378 color: #00b4d8;
379 margin-top: 0;
380}
381
382.comparison-col ul {
383 padding-left: 1.5rem;
384 color: #ccc;
385}
386
387.comparison-col li {
388 margin: 0.5rem 0;
389}
390</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 fait reactive() dans Vue 3 ?
2. Quel est l'avantage de reactive() par rapport à ref() pour les objets ?
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 avec les données de la station (nom, niveau, actif) et affiche-le dans le template.
- Tri horizontal
Range dans le bon ordre les éléments d'une déclaration reactive().
- Éditeur de code
Crée un objet réactif et ajoute des fonctions pour modifier ses propriétés (augmenter le niveau, basculer le statut).
- Clique dans l'ordre
Range dans le bon ordre les étapes de création et de mutation d'un objet reactive.
- Tri vertical
Range dans le bon ordre les étapes du travail avec reactive().