Cours Vue.js · Module 9: Composables et VueUse

VueUse - La bibliothèque de composables

7 min de lecture
Dans cette leçon8

VueUse est une collection de plus de 200 composables prêts à l'emploi - comme la bibliothèque des protocoles de recherche standards de NOVA LAB ! Pourquoi une telle bibliothèque ? Chaque projet ramène les mêmes tâches : mémoriser des réglages dans localStorage, suivre la taille de la fenêtre, la position de la souris ou le mode sombre, retarder une recherche jusqu'à ce que l'utilisateur arrête de taper. Chacune demande d'écouter des événements du navigateur et de faire le ménage ensuite. Tu peux tout réécrire à chaque projet, ou utiliser des fonctions éprouvées et testées du paquet @vueuse/core. Seules les fonctions que tu importes vraiment finissent dans le bundle de l'application.

Installer VueUse

Tu installes la bibliothèque comme n'importe quel paquet npm. La version actuelle de VueUse exige Vue 3.5 ou plus récent :

1npm install @vueuse/core

Après l'installation, tu importes des fonctions individuelles depuis '@vueuse/core', exactement comme ref ou computed depuis 'vue'. Aucun enregistrement de plugin n'est nécessaire.

useLocalStorage

Le premier protocole enregistre les réglages du laboratoire pour qu'ils survivent à un rafraîchissement de la page. useLocalStorage prend une clé et une valeur par défaut, et retourne une ref synchronisée avec le localStorage du navigateur :

1<script setup>
2import { useLocalStorage } from '@vueuse/core'
3
4// Se synchronise automatiquement avec localStorage
5const labSettings = useLocalStorage('lab-settings', {
6  mode: 'exploration',
7  sensorsPerView: 12,
8  sortBy: 'temperature'
9})
10
11// Les changements sont automatiquement enregistrés dans localStorage
12labSettings.value.mode = 'research'
13</script>
14
15<template>
16  <div>
17    <p>Mode : {{ labSettings.mode }}</p>
18    <button @click="labSettings.mode = 'maintenance'">
19      Changer de mode
20    </button>
21  </div>
22</template>

La valeur par défaut n'est utilisée que si rien n'est encore stocké sous la clé lab-settings. Les objets sont enregistrés en JSON, et la modification d'un seul champ est enregistrée elle aussi, car VueUse observe l'objet en profondeur.

useMouseInElement

Ce composable suit la souris par rapport à un élément précis. Il retourne entre autres elementX et elementY, la position mesurée depuis le coin supérieur gauche de l'élément, et isOutside, qui indique si le curseur se trouve en dehors. Les simples x et y sont les coordonnées sur toute la page :

1<script setup>
2import { ref } from 'vue'
3import { useMouseInElement } from '@vueuse/core'
4
5const sensorPanel = ref(null)
6const { elementX, elementY, isOutside } = useMouseInElement(sensorPanel)
7</script>
8
9<template>
10  <div ref="sensorPanel" class="sensor-panel">
11    <img src="/sensor-display.jpg" />
12    <div v-if="!isOutside" class="tooltip" :style="{ left: elementX + 'px', top: elementY + 'px' }">
13      Position : {{ elementX }}, {{ elementY }}
14    </div>
15  </div>
16</template>

La ref sensorPanel se relie à l'élément via l'attribut ref ; depuis Vue 3.5, tu peux aussi utiliser useTemplateRef('sensorPanel'). Pour que l'infobulle suive le curseur, le panneau a besoin de position: relative en CSS et l'infobulle de position: absolute.

useIntersectionObserver

Le protocole suivant vérifie si un élément est visible dans la fenêtre du navigateur. C'est la base du chargement paresseux des images et des animations déclenchées au défilement. Le callback reçoit une liste d'entrées observées, et le champ isIntersecting indique si l'élément est entré dans la zone visible :

1<script setup>
2import { ref } from 'vue'
3import { useIntersectionObserver } from '@vueuse/core'
4
5const experimentPanel = ref(null)
6const isVisible = ref(false)
7
8useIntersectionObserver(
9  experimentPanel,
10  ([{ isIntersecting }]) => {
11    isVisible.value = isIntersecting
12  }
13)
14</script>
15
16<template>
17  <div ref="experimentPanel">
18    <Transition name="fade">
19      <img v-if="isVisible" src="/experiment-data.jpg" />
20    </Transition>
21  </div>
22</template>

L'image n'est téléchargée que lorsque le panneau apparaît à l'écran. VueUse déconnecte lui-même l'observateur au démontage du composant, donc tu n'as pas à écrire ton propre nettoyage.

useWindowSize

useWindowSize retourne les width et height réactifs de la fenêtre, qui se mettent à jour à chaque redimensionnement. À partir d'eux, il est facile de calculer des seuils d'affichage avec computed :

1<script setup>
2import { useWindowSize } from '@vueuse/core'
3import { computed } from 'vue'
4
5const { width, height } = useWindowSize()
6
7const isMobile = computed(() => width.value < 768)
8const isTablet = computed(() => width.value >= 768 && width.value < 1024)
9const isDesktop = computed(() => width.value >= 1024)
10</script>
11
12<template>
13  <div>
14    <p v-if="isMobile">Vue mobile</p>
15    <p v-else-if="isTablet">Vue tablette</p>
16    <p v-else>Vue bureau</p>
17    <p>Dimensions : {{ width }} x {{ height }}</p>
18  </div>
19</template>

Le template change de message sans aucun écouteur resize manuel. Si tu as plus de seuils, VueUse propose pour cela le composable spécialisé useBreakpoints.

useDark & useToggle

useDark retourne une ref contenant la valeur booléenne du mode sombre. Par défaut, il part du réglage du système, ajoute la classe dark à l'élément <html> et mémorise le choix dans localStorage. useToggle transforme une telle ref en fonction de bascule :

1<script setup>
2import { useDark, useToggle } from '@vueuse/core'
3
4const isDark = useDark()
5const toggleDark = useToggle(isDark)
6</script>
7
8<template>
9  <div :class="{ dark: isDark }">
10    <button @click="toggleDark()">
11      Thème {{ isDark ? 'clair' : 'sombre' }}
12    </button>
13  </div>
14</template>
15
16<style>
17body {
18  transition: background 0.3s, color 0.3s;
19}
20
21.dark {
22  background: #1a1a1a;
23  color: #f5f5f5;
24}
25</style>

Après un clic, la classe dark apparaît sur <html> et sur le div qui porte la liaison :class. Cette liaison n'est pas nécessaire, car useDark ajoute lui-même la classe sur <html> ; on la garde pour montrer d'où vient le style.

refDebounced et refThrottled

Le debounce attend qu'une valeur cesse de changer pendant un temps donné, tandis que le throttle laisse passer un changement au plus une fois par intervalle. Dans VueUse, c'est le rôle de refDebounced et refThrottled, qui retournent une copie retardée d'une ref. Les anciens noms useDebounce et useThrottle fonctionnent encore, mais ils sont marqués comme obsolètes :

1<script setup>
2import { ref } from 'vue'
3import { refDebounced, refThrottled } from '@vueuse/core'
4
5// Debounce - attend que l'utilisateur arrête de taper
6const searchQuery = ref('')
7const debouncedQuery = refDebounced(searchQuery, 500)
8
9// Throttle - au maximum une fois toutes les X ms
10const scrollPosition = ref(0)
11const throttledScroll = refThrottled(scrollPosition, 100)
12</script>
13
14<template>
15  <input v-model="searchQuery" placeholder="Rechercher..." />
16  <p>Recherche : {{ debouncedQuery }}</p>
17</template>

Quand tu tapes "oxy", debouncedQuery ne change qu'une demi-seconde après la dernière touche, donc la requête part vers le serveur une seule fois au lieu de trois. Tu mettrais à jour scrollPosition dans un gestionnaire de défilement. Pour retarder des appels de fonctions, VueUse propose aussi useDebounceFn et useThrottleFn.

Composables populaires de VueUse

Pour finir, un aide-mémoire des fonctions que tu utiliseras le plus souvent :

ComposableDescription
useLocalStoragelocalStorage réactif
useFetchFetch avec réactivité
useIntersectionObserverDétection de visibilité
useMouseInElementPosition de la souris dans un élément
useWindowSizeTaille de la fenêtre
useDarkMode sombre
refDebouncedRéactivité retardée (debounce)
useClipboardCopie dans le presse-papiers

Mon conseil : avant d'écrire ton propre composable pour une fonctionnalité du navigateur, jette un œil à vueuse.org. Si la fonction y est déjà, tu obtiens un code avec des tests et un nettoyage correct. Dans l'éditeur ci-dessous, tu verras à quoi ressemble un tel composable de l'intérieur : useConnectionMonitor démarre lui-même ses minuteurs dans onMounted et les nettoie dans onUnmounted - exactement le travail que VueUse fait à ta place.

Retiens : VueUse est un entrepôt de protocoles de station prêts à l'emploi - avant de construire un nouvel instrument, vérifie s'il n'est pas déjà sur l'étagère.

Code de cette leçon : App.vue
1<script setup>
2import { ref, onMounted, onUnmounted } from 'vue'
3
4// Composable avec hooks de cycle de vie : useConnectionMonitor
5function useConnectionMonitor(serverUrl) {
6  const isConnected = ref(false)
7  const connectionTime = ref(null)
8  const disconnectionTime = ref(null)
9  const reconnectAttempts = ref(0)
10  const latency = ref(0)
11  const messages = ref([])
12
13  let intervalId = null
14  let connectionCheckId = null
15
16  function addMessage(type, text) {
17    messages.value.unshift({
18      id: Date.now(),
19      type,
20      text,
21      timestamp: new Date().toLocaleTimeString()
22    })
23    if (messages.value.length > 10) {
24      messages.value.pop()
25    }
26  }
27
28  function connect() {
29    isConnected.value = true
30    connectionTime.value = new Date()
31    reconnectAttempts.value = 0
32    addMessage('success', `Connected to ${serverUrl}`)
33
34    // Simule la surveillance de la latence
35    intervalId = setInterval(() => {
36      latency.value = Math.floor(Math.random() * 100) + 20
37    }, 2000)
38  }
39
40  function disconnect() {
41    isConnected.value = false
42    disconnectionTime.value = new Date()
43    addMessage('warning', `Disconnected from ${serverUrl}`)
44
45    if (intervalId) {
46      clearInterval(intervalId)
47      intervalId = null
48    }
49  }
50
51  function attemptReconnect() {
52    if (isConnected.value) return
53
54    reconnectAttempts.value++
55    addMessage('info', `Reconnection attempt #${reconnectAttempts.value}`)
56
57    setTimeout(() => {
58      if (reconnectAttempts.value <= 3) {
59        connect()
60        addMessage('success', 'Reconnected successfully!')
61      } else {
62        addMessage('error', 'Max reconnection attempts reached')
63      }
64    }, 1500)
65  }
66
67  // Cycle de vie : montage
68  onMounted(() => {
69    addMessage('info', 'Connection monitor initialized')
70
71    // Connexion automatique au montage
72    setTimeout(() => {
73      connect()
74    }, 1000)
75
76    // Simule des déconnexions aléatoires
77    connectionCheckId = setInterval(() => {
78      if (isConnected.value && Math.random() < 0.1) {
79        addMessage('warning', 'Connection lost - random event')
80        disconnect()
81        setTimeout(attemptReconnect, 2000)
82      }
83    }, 5000)
84  })
85
86  // Cycle de vie : démontage
87  onUnmounted(() => {
88    addMessage('info', 'Connection monitor shutting down')
89    if (intervalId) clearInterval(intervalId)
90    if (connectionCheckId) clearInterval(connectionCheckId)
91    disconnect()
92  })
93
94  return {
95    isConnected,
96    connectionTime,
97    disconnectionTime,
98    reconnectAttempts,
99    latency,
100    messages,
101    connect,
102    disconnect,
103    attemptReconnect
104  }
105}
106
107// Crée des moniteurs de connexion pour différents serveurs
108const earthConnection = useConnectionMonitor('Earth Command Center')
109const marsConnection = useConnectionMonitor('Mars Base Alpha')
110const satelliteConnection = useConnectionMonitor('Orbital Satellite')
111</script>
112
113<template>
114  <div class="connection-lab">
115    <div class="header">
116      <h1>Moniteur de connexion - NOVA LAB</h1>
117      <p class="subtitle">Module 3 : composables avec hooks de cycle de vie</p>
118    </div>
119
120    <div class="connections-grid">
121      <!-- Connexion Terre -->
122      <div class="connection-card" :class="{ connected: earthConnection.isConnected }">
123        <div class="connection-header">
124          <h3>Earth Command</h3>
125          <div class="status-indicator" :class="{ active: earthConnection.isConnected }">
126            {{ earthConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
127          </div>
128        </div>
129
130        <div class="connection-stats">
131          <div class="stat-item">
132            <span class="stat-label">Latence :</span>
133            <span class="stat-value" :class="{ good: earthConnection.latency < 50 }">
134              {{ earthConnection.latency }}ms
135            </span>
136          </div>
137          <div class="stat-item">
138            <span class="stat-label">Tentatives de reconnexion :</span>
139            <span class="stat-value">{{ earthConnection.reconnectAttempts }}</span>
140          </div>
141        </div>
142
143        <div class="connection-times">
144          <div v-if="earthConnection.connectionTime" class="time-info">
145            Connecté : {{ earthConnection.connectionTime.toLocaleTimeString() }}
146          </div>
147          <div v-if="earthConnection.disconnectionTime" class="time-info">
148            Déconnecté : {{ earthConnection.disconnectionTime.toLocaleTimeString() }}
149          </div>
150        </div>
151
152        <div class="connection-controls">
153          <button
154            @click="earthConnection.connect"
155            :disabled="earthConnection.isConnected"
156            class="btn btn-success"
157          >
158            Connecter
159          </button>
160          <button
161            @click="earthConnection.disconnect"
162            :disabled="!earthConnection.isConnected"
163            class="btn btn-danger"
164          >
165            Déconnecter
166          </button>
167          <button
168            @click="earthConnection.attemptReconnect"
169            :disabled="earthConnection.isConnected"
170            class="btn btn-warning"
171          >
172            Reconnecter
173          </button>
174        </div>
175
176        <div class="messages-log">
177          <div
178            v-for="msg in earthConnection.messages.slice(0, 5)"
179            :key="msg.id"
180            class="log-message"
181            :class="'msg-' + msg.type"
182          >
183            <span class="msg-time">{{ msg.timestamp }}</span>
184            <span class="msg-text">{{ msg.text }}</span>
185          </div>
186        </div>
187      </div>
188
189      <!-- Mars Connection -->
190      <div class="connection-card" :class="{ connected: marsConnection.isConnected }">
191        <div class="connection-header">
192          <h3>Mars Base Alpha</h3>
193          <div class="status-indicator" :class="{ active: marsConnection.isConnected }">
194            {{ marsConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
195          </div>
196        </div>
197
198        <div class="connection-stats">
199          <div class="stat-item">
200            <span class="stat-label">Latence :</span>
201            <span class="stat-value" :class="{ good: marsConnection.latency < 50 }">
202              {{ marsConnection.latency }}ms
203            </span>
204          </div>
205          <div class="stat-item">
206            <span class="stat-label">Tentatives de reconnexion :</span>
207            <span class="stat-value">{{ marsConnection.reconnectAttempts }}</span>
208          </div>
209        </div>
210
211        <div class="connection-times">
212          <div v-if="marsConnection.connectionTime" class="time-info">
213            Connecté : {{ marsConnection.connectionTime.toLocaleTimeString() }}
214          </div>
215          <div v-if="marsConnection.disconnectionTime" class="time-info">
216            Déconnecté : {{ marsConnection.disconnectionTime.toLocaleTimeString() }}
217          </div>
218        </div>
219
220        <div class="connection-controls">
221          <button
222            @click="marsConnection.connect"
223            :disabled="marsConnection.isConnected"
224            class="btn btn-success"
225          >
226            Connecter
227          </button>
228          <button
229            @click="marsConnection.disconnect"
230            :disabled="!marsConnection.isConnected"
231            class="btn btn-danger"
232          >
233            Déconnecter
234          </button>
235          <button
236            @click="marsConnection.attemptReconnect"
237            :disabled="marsConnection.isConnected"
238            class="btn btn-warning"
239          >
240            Reconnecter
241          </button>
242        </div>
243
244        <div class="messages-log">
245          <div
246            v-for="msg in marsConnection.messages.slice(0, 5)"
247            :key="msg.id"
248            class="log-message"
249            :class="'msg-' + msg.type"
250          >
251            <span class="msg-time">{{ msg.timestamp }}</span>
252            <span class="msg-text">{{ msg.text }}</span>
253          </div>
254        </div>
255      </div>
256
257      <!-- Satellite Connection -->
258      <div class="connection-card" :class="{ connected: satelliteConnection.isConnected }">
259        <div class="connection-header">
260          <h3>Orbital Satellite</h3>
261          <div class="status-indicator" :class="{ active: satelliteConnection.isConnected }">
262            {{ satelliteConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
263          </div>
264        </div>
265
266        <div class="connection-stats">
267          <div class="stat-item">
268            <span class="stat-label">Latence :</span>
269            <span class="stat-value" :class="{ good: satelliteConnection.latency < 50 }">
270              {{ satelliteConnection.latency }}ms
271            </span>
272          </div>
273          <div class="stat-item">
274            <span class="stat-label">Tentatives de reconnexion :</span>
275            <span class="stat-value">{{ satelliteConnection.reconnectAttempts }}</span>
276          </div>
277        </div>
278
279        <div class="connection-times">
280          <div v-if="satelliteConnection.connectionTime" class="time-info">
281            Connecté : {{ satelliteConnection.connectionTime.toLocaleTimeString() }}
282          </div>
283          <div v-if="satelliteConnection.disconnectionTime" class="time-info">
284            Déconnecté : {{ satelliteConnection.disconnectionTime.toLocaleTimeString() }}
285          </div>
286        </div>
287
288        <div class="connection-controls">
289          <button
290            @click="satelliteConnection.connect"
291            :disabled="satelliteConnection.isConnected"
292            class="btn btn-success"
293          >
294            Connecter
295          </button>
296          <button
297            @click="satelliteConnection.disconnect"
298            :disabled="!satelliteConnection.isConnected"
299            class="btn btn-danger"
300          >
301            Déconnecter
302          </button>
303          <button
304            @click="satelliteConnection.attemptReconnect"
305            :disabled="satelliteConnection.isConnected"
306            class="btn btn-warning"
307          >
308            Reconnecter
309          </button>
310        </div>
311
312        <div class="messages-log">
313          <div
314            v-for="msg in satelliteConnection.messages.slice(0, 5)"
315            :key="msg.id"
316            class="log-message"
317            :class="'msg-' + msg.type"
318          >
319            <span class="msg-time">{{ msg.timestamp }}</span>
320            <span class="msg-text">{{ msg.text }}</span>
321          </div>
322        </div>
323      </div>
324    </div>
325
326    <div class="info-panel">
327      <h3>Hooks de cycle de vie dans les composables</h3>
328      <div class="info-content">
329        <p><strong>useConnectionMonitor()</strong> montre comment utiliser les hooks de cycle de vie :</p>
330        <ul>
331          <li><strong>onMounted()</strong> - Connexion automatique et initialisation de la surveillance</li>
332          <li><strong>onUnmounted()</strong> - Nettoyage des intervalles et déconnexion</li>
333          <li>Chaque instance de composable a ses propres hooks de cycle de vie</li>
334          <li>Gestion automatique des ressources (timers, connexions)</li>
335          <li>Prévention des fuites mémoire grâce au nettoyage dans onUnmounted</li>
336        </ul>
337        <div class="code-example">
338          <code>
339function useConnectionMonitor(url) {
340  onMounted(() => {
341    // Setup : connexion automatique, démarrage de la surveillance
342  })
343
344  onUnmounted(() => {
345    // Cleanup : nettoyage des timers, déconnexion
346  })
347
348  return { isConnected, connect, disconnect }
349}
350          </code>
351        </div>
352      </div>
353    </div>
354  </div>
355</template>
356
357<style scoped>
358.connection-lab {
359  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
360  min-height: 100vh;
361  padding: 2rem;
362  color: #ffffff;
363  font-family: 'Courier New', monospace;
364}
365
366.header {
367  text-align: center;
368  margin-bottom: 2rem;
369  padding-bottom: 1rem;
370  border-bottom: 2px solid #00ff88;
371}
372
373.header h1 {
374  font-size: 2.5rem;
375  color: #00ff88;
376  text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
377  margin: 0;
378}
379
380.subtitle {
381  color: #00b4d8;
382  font-size: 1.2rem;
383  margin-top: 0.5rem;
384}
385
386.connections-grid {
387  display: grid;
388  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
389  gap: 1.5rem;
390  margin-bottom: 2rem;
391}
392
393.connection-card {
394  background: rgba(255, 0, 85, 0.1);
395  border: 2px solid #ff0055;
396  border-radius: 10px;
397  padding: 1.5rem;
398  transition: all 0.3s ease;
399}
400
401.connection-card.connected {
402  background: rgba(0, 255, 136, 0.1);
403  border-color: #00ff88;
404}
405
406.connection-header {
407  display: flex;
408  justify-content: space-between;
409  align-items: center;
410  margin-bottom: 1.5rem;
411}
412
413.connection-header h3 {
414  color: #00ff88;
415  margin: 0;
416  font-size: 1.3rem;
417}
418
419.status-indicator {
420  padding: 0.5rem 1rem;
421  border-radius: 20px;
422  background: rgba(255, 0, 85, 0.3);
423  color: #ff0055;
424  font-weight: bold;
425  font-size: 0.9rem;
426}
427
428.status-indicator.active {
429  background: rgba(0, 255, 136, 0.3);
430  color: #00ff88;
431  animation: pulse 2s infinite;
432}
433
434.connection-stats {
435  display: grid;
436  grid-template-columns: 1fr 1fr;
437  gap: 1rem;
438  margin-bottom: 1rem;
439}
440
441.stat-item {
442  background: rgba(0, 0, 0, 0.3);
443  padding: 1rem;
444  border-radius: 5px;
445  text-align: center;
446}
447
448.stat-label {
449  display: block;
450  color: #00b4d8;
451  font-size: 0.8rem;
452  margin-bottom: 0.5rem;
453}
454
455.stat-value {
456  display: block;
457  color: #ffb400;
458  font-size: 1.5rem;
459  font-weight: bold;
460}
461
462.stat-value.good {
463  color: #00ff88;
464}
465
466.connection-times {
467  background: rgba(0, 0, 0, 0.3);
468  padding: 1rem;
469  border-radius: 5px;
470  margin-bottom: 1rem;
471  min-height: 60px;
472}
473
474.time-info {
475  color: #ffffff;
476  font-size: 0.9rem;
477  margin: 0.25rem 0;
478}
479
480.connection-controls {
481  display: flex;
482  gap: 0.5rem;
483  margin-bottom: 1rem;
484}
485
486.messages-log {
487  background: rgba(0, 0, 0, 0.5);
488  border-radius: 5px;
489  padding: 1rem;
490  max-height: 200px;
491  overflow-y: auto;
492}
493
494.log-message {
495  display: flex;
496  gap: 1rem;
497  padding: 0.5rem;
498  margin: 0.25rem 0;
499  border-left: 3px solid;
500  border-radius: 3px;
501  font-size: 0.85rem;
502}
503
504.msg-success {
505  border-color: #00ff88;
506  background: rgba(0, 255, 136, 0.1);
507}
508
509.msg-warning {
510  border-color: #ffb400;
511  background: rgba(255, 180, 0, 0.1);
512}
513
514.msg-error {
515  border-color: #ff0055;
516  background: rgba(255, 0, 85, 0.1);
517}
518
519.msg-info {
520  border-color: #00b4d8;
521  background: rgba(0, 180, 216, 0.1);
522}
523
524.msg-time {
525  color: #00b4d8;
526  min-width: 80px;
527  font-size: 0.75rem;
528}
529
530.msg-text {
531  color: #ffffff;
532  flex: 1;
533}
534
535.btn {
536  flex: 1;
537  padding: 0.75rem;
538  border: none;
539  border-radius: 5px;
540  font-weight: bold;
541  cursor: pointer;
542  font-family: 'Courier New', monospace;
543  transition: all 0.3s ease;
544  font-size: 0.9rem;
545}
546
547.btn:disabled {
548  opacity: 0.5;
549  cursor: not-allowed;
550}
551
552.btn-success {
553  background: #00ff88;
554  color: #0a0e27;
555}
556
557.btn-danger {
558  background: #ff0055;
559  color: #ffffff;
560}
561
562.btn-warning {
563  background: #ffb400;
564  color: #0a0e27;
565}
566
567.btn:not(:disabled):hover {
568  transform: scale(1.05);
569  box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
570}
571
572.info-panel {
573  background: rgba(0, 180, 216, 0.05);
574  border: 2px solid #00b4d8;
575  border-radius: 10px;
576  padding: 2rem;
577}
578
579.info-panel h3 {
580  color: #00ff88;
581  margin-top: 0;
582  margin-bottom: 1.5rem;
583}
584
585.info-content ul {
586  list-style: none;
587  padding: 0;
588}
589
590.info-content li {
591  padding: 0.5rem 0;
592  color: #ffffff;
593  line-height: 1.6;
594}
595
596.code-example {
597  background: rgba(0, 0, 0, 0.5);
598  border-left: 4px solid #00ff88;
599  border-radius: 5px;
600  padding: 1rem;
601  margin-top: 1rem;
602}
603
604.code-example code {
605  color: #00ff88;
606  font-size: 0.9rem;
607  white-space: pre;
608  display: block;
609}
610
611@keyframes pulse {
612  0%, 100% { opacity: 1; }
613  50% { opacity: 0.6; }
614}
615</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 propose la bibliothèque VueUse ?

  2. 2. À quoi sert useLocalStorage de VueUse ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un composant responsive avec les computed isMobile, isTablet, isDesktop.

  • Tri vertical

    Ordonne les étapes pour installer et utiliser VueUse :

  • Clique dans l'ordre

    Ordonne l'import d'un composable depuis VueUse :

  • Éditeur de code

    Utilise useDark et useToggle pour créer une bascule de mode sombre avec la classe CSS appropriée.

  • Tri horizontal

    Ordonne la déstructuration de useWindowSize :