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

reactive() - Les systèmes réactifs

5 min de lecture
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 panneau

Le 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) // true

Le 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éristiqueref()reactive()
Pour les primitivesOuiNon
Pour les objetsOui (.value)Oui (directement)
DéstructurationSûre (tu passes tout le conteneur)Perd la réactivité
RéaffectationOui (.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éactif

La 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. 1. Que fait reactive() dans Vue 3 ?

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