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

La réactivité en pratique - Le panneau de contrôle

7 min de lecture
Dans cette leçon5

Combinons tout dans des exemples pratiques de systèmes du NOVA LAB.

Jusqu'ici, tu as observé chaque outil de réactivité séparément. Les vrais panneaux de la station les combinent tous à la fois : le formulaire de configuration doit valider les données à la volée, le moniteur des modules filtre la liste selon la phrase saisie, et le système d'approvisionnement calcule les totaux de la commande. Les trois panneaux ci-dessous partagent la même organisation en couches. D'abord les données dans ref() ou reactive(), ensuite les valeurs dérivées avec computed(), puis les actions, c'est-à-dire les fonctions qui modifient l'état, et enfin le template avec des liaisons réactives.

Panneau de configuration du réacteur avec validation

Le template du formulaire utilise v-model, la liaison bidirectionnelle entre un champ et les données, que tu découvriras en détail dans le module 5. Le modificateur .number transforme le texte saisi en nombre, et tu connais @submit.prevent et :disabled depuis le module 2 :

1<template>
2  <form @submit.prevent="submitConfig" class="reactor-config" style="background: #0a0e27; color: #00ff88;">
3    <div>
4      <label>Nom du module :</label>
5      <input v-model="config.name" style="background: #1a1e37; color: #00ff88;" />
6      <span v-if="errors.name" style="color: #ff4444;">{{ errors.name }}</span>
7    </div>
8
9    <div>
10      <label>Température maximale (K) :</label>
11      <input v-model.number="config.maxTemp" type="number" style="background: #1a1e37; color: #00ff88;" />
12      <span v-if="errors.maxTemp" style="color: #ff4444;">{{ errors.maxTemp }}</span>
13    </div>
14
15    <div>
16      <label>Statut :</label>
17      <input v-model="config.status" style="background: #1a1e37; color: #00ff88;" />
18    </div>
19
20    <button :disabled="!isValid" style="background: #00b4d8; color: #0a0e27;">Enregistrer la configuration</button>
21  </form>
22</template>

Les messages d'erreur apparaissent via v-if seulement quand il y a quelque chose à montrer, et le bouton d'enregistrement reste bloqué tant que le formulaire n'est pas valide. Les données du formulaire forment un groupe cohérent de champs, donc nous les gardons dans reactive() :

1<script setup>
2import { reactive, computed } from 'vue'
3
4const config = reactive({
5  name: '',
6  maxTemp: null,
7  status: ''
8})

La validation est une valeur dérivée de ces champs. Dans la première version de ce panneau, la propriété computed écrivait elle-même les messages dans un objet errors séparé. La documentation de Vue interdit pourtant les effets de bord dans les computed : un getter doit seulement calculer et renvoyer un résultat. C'est pourquoi les erreurs elles-mêmes sont ici des computed :

1// La validation comme valeur dérivée - sans effets de bord
2const errors = computed(() => ({
3  // Validation du nom
4  name: config.name.length < 3 ? 'Le nom doit comporter au moins 3 caractères' : '',
5  // Validation de la température
6  maxTemp: config.maxTemp && (config.maxTemp < 1000 || config.maxTemp > 10000)
7    ? 'La température doit être comprise entre 1000 et 10000 K'
8    : ''
9}))
10
11const isValid = computed(() => !errors.value.name && !errors.value.maxTemp)

Le template n'a demandé aucun changement : errors est un ref de niveau supérieur, donc errors.name y fonctionne sans .value. La saisie du troisième caractère du nom recalcule les deux computed et débloque le bouton. L'action qui envoie la configuration ne lit plus que le résultat final :

1function submitConfig() {
2  if (isValid.value) {
3    console.log('Envoi de la configuration au NOVA LAB :', config)
4  }
5}
6</script>

Système de surveillance des modules

Le moniteur combine un champ de recherche et une liste déroulante avec une liste filtrée de modules. v-model fonctionne aussi sur <select> :

1<template>
2  <div class="module-monitor" style="background: #0a0e27; color: #00ff88;">
3    <input v-model="searchQuery" placeholder="Rechercher un module..." style="background: #1a1e37; color: #00ff88;" />
4    <select v-model="filterStatus" style="background: #1a1e37; color: #00ff88;">
5      <option value="">Tous les statuts</option>
6      <option value="ACTIVE">Actif</option>
7      <option value="STANDBY">En veille</option>
8    </select>
9
10    <ul>
11      <li v-for="module in filteredModules" :key="module.id" style="border-bottom: 1px solid #00b4d8;">
12        {{ module.name }} - {{ module.status }}
13      </li>
14    </ul>
15
16    <p>Trouvés : {{ filteredModules.length }} modules</p>
17  </div>
18</template>

Le template ne filtre rien lui-même - il reçoit la liste prête filteredModules. L'état se compose de deux refs simples et d'un tableau de modules :

1<script setup>
2import { ref, computed } from 'vue'
3
4const searchQuery = ref('')
5const filterStatus = ref('')
6
7const modules = ref([
8  { id: 1, name: 'Oxygen Generator Alpha', status: 'ACTIVE' },
9  { id: 2, name: 'Plasma Core Beta', status: 'STANDBY' },
10  { id: 3, name: 'Water Recycler Gamma', status: 'ACTIVE' },
11  { id: 4, name: 'Power Distributor Delta', status: 'STANDBY' }
12])

Le filtre combine les deux conditions. L'écriture !filterStatus.value || signifie : quand aucun statut n'est choisi, laisse passer chaque module :

1const filteredModules = computed(() => {
2  return modules.value.filter(module => {
3    const matchesSearch = module.name
4      .toLowerCase()
5      .includes(searchQuery.value.toLowerCase())
6
7    const matchesStatus = !filterStatus.value ||
8      module.status === filterStatus.value
9
10    return matchesSearch && matchesStatus
11  })
12})
13</script>

Le tableau original modules reste intact, et la liste à l'écran se rafraîchit à chaque caractère saisi.

Système d'approvisionnement de la mission

La commande d'approvisionnement affiche le nombre d'articles, les lignes avec un bouton de suppression et le total en crédits :

1<template>
2  <div class="supply-cart" style="background: #0a0e27; color: #00ff88;">
3    <h2>Commande ({{ supplies.itemCount }} articles)</h2>
4
5    <div v-for="item in supplies.items" :key="item.id" style="border-bottom: 1px solid #00b4d8;">
6      {{ item.name }} - {{ item.quantity }} x {{ item.price }} CR
7      <button @click="removeItem(item.id)" style="background: #ff4444; color: #fff;">Retirer</button>
8    </div>
9
10    <p><strong>Total : {{ supplies.total }} CR (Crédits)</strong></p>
11  </div>
12</template>

Ici, les valeurs dérivées sont écrites autrement que dans les panneaux précédents : sous forme de getters à l'intérieur même de l'objet réactif. Un getter est une propriété JavaScript dont la valeur est calculée à chaque lecture par une fonction marquée du mot-clé get :

1<script setup>
2import { reactive } from 'vue'
3
4const supplies = reactive({
5  items: [
6    { id: 1, name: 'Oxygen Canister', quantity: 10, price: 50 },
7    { id: 2, name: 'Plasma Cell', quantity: 5, price: 200 }
8  ],
9
10  // Calculé sous forme de propriété
11  get itemCount() {
12    return this.items.reduce((sum, item) => sum + item.quantity, 0)
13  },
14
15  get total() {
16    return this.items.reduce((sum, item) =>
17      sum + item.quantity * item.price, 0)
18  }
19})

Au départ, itemCount vaut 15 et total 1500 CR. Un getter calcule son résultat à chaque lecture et, contrairement à computed(), ne le met pas en cache, ce qui n'a pas d'importance pour une liste courte. L'action de suppression modifie le tableau sur place :

1function removeItem(id) {
2  const index = supplies.items.findIndex(item => item.id === id)
3  if (index > -1) {
4    supplies.items.splice(index, 1)
5  }
6}
7</script>

Après le retrait des cellules à plasma, le total tombe à 500 CR, car splice est une méthode que Vue détecte.

readonly() - Un affichage sans droit d'écriture

Le moniteur du mess doit afficher l'état du réacteur, mais il ne doit pas pouvoir le modifier. readonly() renvoie un Proxy profond en lecture seule :

1import { reactive, readonly } from 'vue'
2
3const reactorState = reactive({ temperature: 2500 })
4const reactorView = readonly(reactorState)
5
6reactorView.temperature = 9999 // avertissement dans la console, changement refusé
7reactorState.temperature = 2600 // un changement via l'original fonctionne
8console.log(reactorView.temperature) // 2600

En mode développement, une tentative d'écriture se termine par un avertissement de Vue, et la valeur reste inchangée. La vue en lecture seule n'est pas une copie : elle montre chaque changement de l'original. Le modèle est simple - tu gardes l'état modifiable pour toi, tu partages la version readonly avec l'extérieur, et tu ne fais les changements que par tes propres fonctions.

Choisir le bon outil

La documentation de Vue recommande ref() comme outil principal. reactive() convient aux groupes cohérents de champs, shallowRef() avec triggerRef() aux énormes listes, markRaw() aux instances de bibliothèques externes, et readonly() au partage de l'état. Sous le capot, Vue 3 intercepte l'accès aux objets avec le Proxy natif, et l'accès aux refs avec des getters et des setters. Tiens-toi à la séparation entre données, valeurs dérivées, actions et template et chaque panneau restera lisible. Dans le projet du module, tu construiras à partir de ces couches un système complet de surveillance du réacteur.

Retiens : un bon panneau de station a une seule source de vérité, des valeurs dérivées au lieu de copies manuelles, et des actions qui sont les seules à toucher l'état.

Code de cette leçon : App.vue
1<script setup>
2import { ref, reactive, markRaw, isReactive, toRaw } from 'vue'
3
4// markRaw() - Contrôle du réacteur à plasma
5// Contrôle avancé de la réactivité au NOVA LAB
6
7// Simulateur de gros objet (ex. bibliothèque 3D)
8class HeavySimulationEngine {
9  constructor(name) {
10    this.name = name
11    this.iterations = 0
12    this.data = new Array(1000).fill(0).map((_, i) => ({
13      id: i,
14      value: Math.random() * 100
15    }))
16  }
17
18  simulate() {
19    this.iterations++
20    // Calculs lourds
21    return this.data.reduce((sum, item) => sum + item.value, 0)
22  }
23
24  reset() {
25    this.iterations = 0
26  }
27}
28
29// Réacteur avec objet normal (réactif)
30const reactiveReactor = reactive({
31  id: 'REACTOR-A',
32  status: 'ONLINE',
33  power: 100,
34  engine: new HeavySimulationEngine('Reactive Engine'),
35  lastUpdate: new Date().toISOString()
36})
37
38// Réacteur avec markRaw (moteur non réactif)
39const optimizedReactor = reactive({
40  id: 'REACTOR-B',
41  status: 'ONLINE',
42  power: 100,
43  engine: markRaw(new HeavySimulationEngine('Optimized Engine')),
44  lastUpdate: new Date().toISOString()
45})
46
47// Vérification de la réactivité
48const reactiveEngineIsReactive = isReactive(reactiveReactor.engine)
49const optimizedEngineIsReactive = isReactive(optimizedReactor.engine)
50
51// Compteurs pour la démonstration des performances
52const reactiveSimulationCount = ref(0)
53const optimizedSimulationCount = ref(0)
54
55function runReactiveSimulation() {
56  const start = performance.now()
57
58  // Modification du moteur réactif
59  reactiveReactor.engine.simulate()
60  reactiveReactor.lastUpdate = new Date().toISOString()
61  reactiveSimulationCount.value++
62
63  const end = performance.now()
64  console.log(`Reactive simulation: ${(end - start).toFixed(2)}ms`)
65}
66
67function runOptimizedSimulation() {
68  const start = performance.now()
69
70  // Modification du moteur markRaw
71  optimizedReactor.engine.simulate()
72  optimizedReactor.lastUpdate = new Date().toISOString()
73  optimizedSimulationCount.value++
74
75  const end = performance.now()
76  console.log(`Optimized simulation: ${(end - start).toFixed(2)}ms`)
77}
78
79// Exemple pratique - map avec beaucoup d'objets
80const sensorNetwork = reactive({
81  location: 'Mars Base Alpha',
82  activeSensors: 0,
83  sensors: markRaw(new Map([
84    ['TEMP-001', { type: 'temperature', value: 22 }],
85    ['PRES-001', { type: 'pressure', value: 101 }],
86    ['OXY-001', { type: 'oxygen', value: 21 }]
87  ]))
88})
89
90function addSensor() {
91  const id = `SENS-${Date.now()}`
92  sensorNetwork.sensors.set(id, {
93    type: 'generic',
94    value: Math.random() * 100
95  })
96  // On met à jour le compteur réactif
97  sensorNetwork.activeSensors = sensorNetwork.sensors.size
98}
99
100function removeSensor() {
101  const keys = Array.from(sensorNetwork.sensors.keys())
102  if (keys.length > 0) {
103    sensorNetwork.sensors.delete(keys[keys.length - 1])
104    sensorNetwork.activeSensors = sensorNetwork.sensors.size
105  }
106}
107
108// toRaw() - accès à l'objet original
109function getRawReactor() {
110  const raw = toRaw(reactiveReactor)
111  console.log('Raw reactor (non-reactive):', raw)
112  console.log('Is reactive?', isReactive(raw)) // false
113}
114
115// Exemple d'intégration d'une bibliothèque externe
116const chartConfig = reactive({
117  title: 'Reactor Power Output',
118  // Grosse bibliothèque de graphiques - on n'a pas besoin qu'elle soit réactive
119  chartInstance: markRaw({
120    render() {
121      return 'Chart rendered'
122    },
123    update(data) {
124      console.log('Chart updated with:', data)
125    },
126    destroy() {
127      console.log('Chart destroyed')
128    }
129  })
130})
131
132function updateChart() {
133  chartConfig.chartInstance.update({
134    power: reactiveReactor.power,
135    timestamp: new Date()
136  })
137}
138</script>
139
140<template>
141  <div class="mark-raw-demo">
142    <div class="header">
143      <h1>CONTRÔLE DU RÉACTEUR - NOVA LAB</h1>
144      <p class="subtitle">markRaw() et toRaw() - Contrôle avancé de la réactivité</p>
145    </div>
146
147    <!-- Comparaison des performances -->
148    <div class="comparison-section">
149      <h2>Comparaison des performances : Reactive vs markRaw</h2>
150
151      <div class="reactors-grid">
152        <!-- Réacteur A - Reactive -->
153        <div class="reactor-card">
154          <div class="reactor-header">
155            <h3>{{ reactiveReactor.id }}</h3>
156            <span class="badge badge-reactive">REACTIVE</span>
157          </div>
158
159          <div class="reactor-stats">
160            <div class="stat">
161              <div class="label">Status</div>
162              <div class="value">{{ reactiveReactor.status }}</div>
163            </div>
164            <div class="stat">
165              <div class="label">Puissance</div>
166              <div class="value">{{ reactiveReactor.power }}%</div>
167            </div>
168            <div class="stat">
169              <div class="label">Itérations du moteur</div>
170              <div class="value">{{ reactiveReactor.engine.iterations }}</div>
171            </div>
172            <div class="stat">
173              <div class="label">Moteur réactif ?</div>
174              <div class="value">{{ reactiveEngineIsReactive ? 'OUI' : 'NON' }}</div>
175            </div>
176          </div>
177
178          <div class="simulation-info">
179            <p>Simulations lancées : <strong>{{ reactiveSimulationCount }}</strong></p>
180            <p class="warning">Chaque changement dans le moteur déclenche la réactivité !</p>
181          </div>
182
183          <button @click="runReactiveSimulation" class="btn btn-primary">
184            ▶Lancer la simulation
185          </button>
186
187          <div class="code-box">
188            <pre>// Objet normal - réactif
189const reactor = reactive({
190  engine: new Engine()
191})
192// engine et toutes ses propriétés
193// sont réactifs Plus lent !</pre>
194          </div>
195        </div>
196
197        <!-- Reactor B - Optimized -->
198        <div class="reactor-card">
199          <div class="reactor-header">
200            <h3>{{ optimizedReactor.id }}</h3>
201            <span class="badge badge-optimized">OPTIMIZED</span>
202          </div>
203
204          <div class="reactor-stats">
205            <div class="stat">
206              <div class="label">Status</div>
207              <div class="value">{{ optimizedReactor.status }}</div>
208            </div>
209            <div class="stat">
210              <div class="label">Puissance</div>
211              <div class="value">{{ optimizedReactor.power }}%</div>
212            </div>
213            <div class="stat">
214              <div class="label">Itérations du moteur</div>
215              <div class="value">{{ optimizedReactor.engine.iterations }}</div>
216            </div>
217            <div class="stat">
218              <div class="label">Moteur réactif ?</div>
219              <div class="value">{{ optimizedEngineIsReactive ? 'OUI' : 'NON' }}</div>
220            </div>
221          </div>
222
223          <div class="simulation-info">
224            <p>Simulations lancées : <strong>{{ optimizedSimulationCount }}</strong></p>
225            <p class="success">Le moteur n'est pas réactif - plus performant !</p>
226          </div>
227
228          <button @click="runOptimizedSimulation" class="btn btn-success">
229            Lancer la simulation (plus rapide)
230          </button>
231
232          <div class="code-box">
233            <pre>// markRaw - non réactif
234const reactor = reactive({
235  engine: markRaw(new Engine())
236})
237// engine N'EST PAS réactif
238// Plus rapide !</pre>
239          </div>
240        </div>
241      </div>
242    </div>
243
244    <!-- Sensor network -->
245    <div class="sensors-section">
246      <h2>Réseau de capteurs (exemple pratique de markRaw)</h2>
247
248      <div class="sensors-card">
249        <div class="sensors-header">
250          <h3>{{ sensorNetwork.location }}</h3>
251          <div class="sensor-count">
252            Capteurs actifs : <strong>{{ sensorNetwork.activeSensors }}</strong>
253          </div>
254        </div>
255
256        <div class="sensors-grid">
257          <div
258            v-for="[id, sensor] in sensorNetwork.sensors"
259            :key="id"
260            class="sensor-item"
261          >
262            <div class="sensor-id">{{ id }}</div>
263            <div class="sensor-type">{{ sensor.type }}</div>
264            <div class="sensor-value">{{ sensor.value.toFixed(2) }}</div>
265          </div>
266        </div>
267
268        <div class="controls">
269          <button @click="addSensor" class="btn btn-success">
270            Ajouter un capteur
271          </button>
272          <button @click="removeSensor" class="btn btn-danger">
273            Retirer un capteur
274          </button>
275        </div>
276
277        <div class="info">
278          <p>La map des capteurs utilise <code>markRaw()</code></p>
279          <p>Seul le compteur <code>activeSensors</code> est réactif</p>
280        </div>
281
282        <div class="code-box">
283          <pre>const network = reactive({
284  activeSensors: 0,
285  // La Map n'a pas besoin d'être réactive
286  sensors: markRaw(new Map())
287})
288
289// Ne mets à jour que le compteur réactif
290network.activeSensors = network.sensors.size</pre>
291        </div>
292      </div>
293    </div>
294
295    <!-- Intégration de bibliothèques -->
296    <div class="library-section">
297      <h2>Intégration avec des bibliothèques externes</h2>
298
299      <div class="library-card">
300        <h3>{{ chartConfig.title }}</h3>
301
302        <div class="chart-placeholder">
303          <div class="chart-visual">
304            Graphique de la puissance du réacteur
305          </div>
306          <p>Instance du graphique : {{ chartConfig.chartInstance.render() }}</p>
307        </div>
308
309        <button @click="updateChart" class="btn btn-info">
310          Mettre à jour le graphique
311        </button>
312
313        <div class="code-box">
314          <pre>import Chart from 'chart-library'
315
316const config = reactive({
317  title: 'My Chart',
318  // Grosse bibliothèque - on n'a pas besoin de réactivité
319  chartInstance: markRaw(new Chart())
320})
321
322// Mise à jour manuelle quand nécessaire
323config.chartInstance.update(newData)</pre>
324        </div>
325      </div>
326    </div>
327
328    <!-- toRaw() demo -->
329    <div class="toraw-section">
330      <h2>toRaw() - Accès à l'objet original</h2>
331
332      <div class="toraw-card">
333        <p>Parfois, tu as besoin d'accéder à l'objet original, non réactif</p>
334
335        <button @click="getRawReactor" class="btn btn-warning">
336          Obtenir le réacteur RAW (regarde la console)
337        </button>
338
339        <div class="code-box">
340          <pre>const reactive = reactive({ value: 1 })
341
342// Récupère l'objet original
343const raw = toRaw(reactive)
344
345// Les changements dans raw ne déclencheront PAS de réactivité
346raw.value = 2
347
348console.log(isReactive(raw)) // false</pre>
349        </div>
350
351        <div class="use-cases">
352          <h4>Quand utiliser toRaw() ?</h4>
353          <ul>
354            <li>Passer à une API qui n'a pas besoin de réactivité</li>
355            <li>Opérations sur des données brutes sans déclencher la réactivité</li>
356            <li>Débogage - vérifier les valeurs originales</li>
357          </ul>
358        </div>
359      </div>
360    </div>
361
362    <!-- Info box -->
363    <div class="info-box">
364      <h3>Quand utiliser markRaw() ?</h3>
365      <div class="use-cases-grid">
366        <div class="use-case">
367          <h4>Gros objets de données</h4>
368          <p>Structures de données gigantesques qui n'ont pas besoin d'être réactives</p>
369        </div>
370        <div class="use-case">
371          <h4>Bibliothèques externes</h4>
372          <p>Instances de Chart.js, Three.js, D3.js - pas besoin de réactivité</p>
373        </div>
374        <div class="use-case">
375          <h4>Map, Set, WeakMap</h4>
376          <p>Collections que tu changes souvent, mais dont tu n'as pas besoin de suivre chaque changement</p>
377        </div>
378        <div class="use-case">
379          <h4>Optimisation des performances</h4>
380          <p>Quand la réactivité ralentit l'application inutilement</p>
381        </div>
382      </div>
383
384      <div class="warning-box">
385        <h4>Attention !</h4>
386        <p><code>markRaw()</code> est irréversible - l'objet ne deviendra jamais réactif !</p>
387        <p>À utiliser uniquement quand tu es sûr que la réactivité n'est pas nécessaire.</p>
388      </div>
389    </div>
390  </div>
391</template>
392
393<style scoped>
394.mark-raw-demo {
395  min-height: 100vh;
396  background: #0a0e27;
397  color: #e0e0e0;
398  padding: 2rem;
399  font-family: 'Courier New', monospace;
400}
401
402.header {
403  text-align: center;
404  margin-bottom: 2rem;
405  border-bottom: 2px solid #00ff88;
406  padding-bottom: 1rem;
407}
408
409.header h1 {
410  color: #00ff88;
411  font-size: 2rem;
412  margin: 0;
413  text-shadow: 0 0 10px #00ff88;
414}
415
416.subtitle {
417  color: #00b4d8;
418  margin: 0.5rem 0 0 0;
419}
420
421.comparison-section,
422.sensors-section,
423.library-section,
424.toraw-section {
425  background: #1a1f3a;
426  padding: 1.5rem;
427  border-radius: 8px;
428  border: 1px solid #00b4d8;
429  margin-bottom: 2rem;
430}
431
432h2 {
433  color: #00ff88;
434  margin-top: 0;
435}
436
437.reactors-grid {
438  display: grid;
439  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
440  gap: 1.5rem;
441}
442
443.reactor-card,
444.sensors-card,
445.library-card,
446.toraw-card {
447  background: #0a0e27;
448  padding: 1.5rem;
449  border-radius: 6px;
450}
451
452.reactor-header,
453.sensors-header {
454  display: flex;
455  justify-content: space-between;
456  align-items: center;
457  margin-bottom: 1.5rem;
458  padding-bottom: 1rem;
459  border-bottom: 1px solid #333;
460}
461
462.reactor-header h3,
463.sensors-header h3 {
464  color: #00b4d8;
465  margin: 0;
466}
467
468.badge {
469  padding: 0.3rem 0.8rem;
470  border-radius: 20px;
471  font-size: 0.8rem;
472  font-weight: bold;
473}
474
475.badge-reactive {
476  background: #ff8800;
477  color: #0a0e27;
478}
479
480.badge-optimized {
481  background: #00ff88;
482  color: #0a0e27;
483}
484
485.reactor-stats {
486  display: grid;
487  grid-template-columns: repeat(2, 1fr);
488  gap: 1rem;
489  margin-bottom: 1.5rem;
490}
491
492.stat {
493  background: #1a1f3a;
494  padding: 1rem;
495  border-radius: 4px;
496}
497
498.stat .label {
499  font-size: 0.85rem;
500  color: #888;
501  margin-bottom: 0.3rem;
502}
503
504.stat .value {
505  font-size: 1.2rem;
506  color: #00ff88;
507  font-weight: bold;
508}
509
510.simulation-info {
511  background: #1a1f3a;
512  padding: 1rem;
513  border-radius: 4px;
514  margin-bottom: 1rem;
515}
516
517.simulation-info p {
518  margin: 0.5rem 0;
519}
520
521.simulation-info strong {
522  color: #00ff88;
523}
524
525.warning {
526  color: #ff8800;
527}
528
529.success {
530  color: #00ff88;
531}
532
533.btn {
534  width: 100%;
535  padding: 1rem;
536  font-size: 1rem;
537  border: none;
538  border-radius: 6px;
539  cursor: pointer;
540  font-weight: bold;
541  transition: all 0.3s ease;
542  font-family: 'Courier New', monospace;
543  margin-bottom: 1rem;
544}
545
546.btn-primary {
547  background: linear-gradient(135deg, #00b4d8, #0088cc);
548  color: white;
549}
550
551.btn-success {
552  background: linear-gradient(135deg, #00ff88, #00dd77);
553  color: #0a0e27;
554}
555
556.btn-danger {
557  background: linear-gradient(135deg, #ff4444, #cc0000);
558  color: white;
559}
560
561.btn-info {
562  background: linear-gradient(135deg, #00b4d8, #00ff88);
563  color: #0a0e27;
564}
565
566.btn-warning {
567  background: linear-gradient(135deg, #ffaa00, #ff8800);
568  color: #0a0e27;
569}
570
571.btn:hover {
572  transform: scale(1.02);
573  box-shadow: 0 0 20px currentColor;
574}
575
576.code-box {
577  background: #1a1f3a;
578  padding: 1rem;
579  border-radius: 4px;
580  border-left: 3px solid #00ff88;
581}
582
583.code-box pre {
584  margin: 0;
585  color: #00ff88;
586  font-size: 0.85rem;
587  overflow-x: auto;
588}
589
590.sensor-count {
591  color: #00ff88;
592  font-weight: bold;
593}
594
595.sensors-grid {
596  display: grid;
597  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
598  gap: 1rem;
599  margin: 1.5rem 0;
600}
601
602.sensor-item {
603  background: #1a1f3a;
604  padding: 1rem;
605  border-radius: 4px;
606  border-left: 3px solid #00b4d8;
607}
608
609.sensor-id {
610  color: #00b4d8;
611  font-weight: bold;
612  font-size: 0.9rem;
613}
614
615.sensor-type {
616  color: #888;
617  font-size: 0.85rem;
618  margin: 0.3rem 0;
619  text-transform: uppercase;
620}
621
622.sensor-value {
623  color: #00ff88;
624  font-size: 1.2rem;
625  font-weight: bold;
626}
627
628.controls {
629  display: flex;
630  gap: 1rem;
631}
632
633.controls .btn {
634  flex: 1;
635}
636
637.info {
638  background: #1a1f3a;
639  padding: 1rem;
640  border-radius: 4px;
641  margin-top: 1rem;
642}
643
644.info p {
645  margin: 0.5rem 0;
646  color: #ccc;
647}
648
649.info code {
650  background: #0a0e27;
651  padding: 0.2rem 0.5rem;
652  border-radius: 3px;
653  color: #00ff88;
654}
655
656.chart-placeholder {
657  background: #1a1f3a;
658  padding: 2rem;
659  border-radius: 4px;
660  text-align: center;
661  margin: 1rem 0;
662}
663
664.chart-visual {
665  font-size: 3rem;
666  margin-bottom: 1rem;
667}
668
669.use-cases {
670  background: #1a1f3a;
671  padding: 1rem;
672  border-radius: 4px;
673  margin-top: 1rem;
674}
675
676.use-cases h4 {
677  color: #00b4d8;
678  margin-top: 0;
679}
680
681.use-cases ul {
682  color: #ccc;
683  padding-left: 1.5rem;
684}
685
686.use-cases li {
687  margin: 0.5rem 0;
688}
689
690.info-box {
691  background: #1a1f3a;
692  padding: 1.5rem;
693  border-radius: 8px;
694  border-left: 4px solid #00ff88;
695}
696
697.info-box h3 {
698  color: #00ff88;
699  margin-top: 0;
700}
701
702.use-cases-grid {
703  display: grid;
704  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
705  gap: 1rem;
706  margin: 1rem 0;
707}
708
709.use-case {
710  background: #0a0e27;
711  padding: 1rem;
712  border-radius: 4px;
713}
714
715.use-case h4 {
716  color: #00b4d8;
717  margin-top: 0;
718}
719
720.use-case p {
721  color: #ccc;
722  margin: 0;
723  font-size: 0.9rem;
724}
725
726.warning-box {
727  background: #2a1f1a;
728  border: 2px solid #ff8800;
729  padding: 1rem;
730  border-radius: 4px;
731  margin-top: 1rem;
732}
733
734.warning-box h4 {
735  color: #ffaa00;
736  margin-top: 0;
737}
738
739.warning-box p {
740  color: #ccc;
741  margin: 0.5rem 0;
742}
743
744.warning-box code {
745  background: #0a0e27;
746  padding: 0.2rem 0.5rem;
747  border-radius: 3px;
748  color: #00ff88;
749}
750</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. Quelle est l'approche recommandée pour la réactivité dans Vue 3 ?

  2. 2. Quel mécanisme Vue 3 utilise-t-il pour implémenter la réactivité ?

Voici 2 des 11 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 enveloppe-le dans readonly() pour le protéger contre les changements.

  • Tri vertical

    Range dans le bon ordre les étapes d'utilisation de readonly dans un composant.

  • Clique dans l'ordre

    Range dans le bon ordre les étapes d'utilisation de markRaw avec une bibliothèque externe.

  • Éditeur de code

    Utilise markRaw() pour un objet qui n'a pas besoin de réactivité (par exemple une instance de bibliothèque ou une configuration).

  • Tri vertical

    Range dans le bon ordre les étapes d'optimisation avec triggerRef().

  • Clique dans l'ordre

    Range dans le bon ordre l'instruction d'import des fonctions réactives.

  • Tri vertical

    Range les concepts de réactivité du plus simple au plus avancé.

  • Tri vertical

    Range dans le bon ordre les étapes d'utilisation de shallowRef.

  • Tri vertical

    Range dans le bon ordre les étapes de conception de l'état dans un composant.

  • Clique dans l'ordre

    Range dans le bon ordre les étapes de création d'un formulaire réactif.