Cours Vue.js · Module 6: Composants
Composables - Réutiliser la logique
Dans cette leçon7
Le Dr Nova explique : « Au NOVA LAB, de nombreux modules ont besoin des mêmes systèmes - minuteur de mission, relevés des capteurs, connexion à la base de données. Au lieu de copier le même code dans chaque module, nous créons des systèmes partagés. Dans Vue, un tel système s'appelle un composable. »
Cinq panneaux de la station affichent le temps de mission. Si chacun a sa propre copie du code avec setInterval, onMounted et onUnmounted, alors corriger un bug dans le calcul du temps oblige à modifier cinq fichiers, et en oublier un donne cinq horloges différentes sur une seule station.
Qu'est-ce qu'un composable ?
Un composable est une fonction qui encapsule et renvoie un état réactif et de la logique. Il permet de partager de la logique entre plusieurs composants sans dupliquer le code. La documentation de Vue le décrit comme une fonction qui s'appuie sur la Composition API, c'est-à-dire sur ref, computed ou les hooks du cycle de vie.
Convention de nommage : un composable commence toujours par use - par ex. useTimer, useCounter, useMarsData. L'exemple le plus simple est un compteur composé d'un conteneur, de trois fonctions et d'un objet qui les renvoie :
1// composables/useCounter.js
2import { ref } from 'vue'
3
4export function useCounter(initialValue = 0) {
5 const count = ref(initialValue)
6
7 function increment() {
8 count.value++
9 }
10
11 function decrement() {
12 count.value--
13 }
14
15 function reset() {
16 count.value = initialValue
17 }
18
19 // Renvoie l'état réactif et les fonctions
20 return { count, increment, decrement, reset }
21}C'est une fonction JavaScript ordinaire exportée depuis un fichier .js - sans template et sans styles. Elle renvoie un objet ordinaire avec des conteneurs et des fonctions, donc un composant peut le déstructurer et count reste réactif. Le paramètre initialValue = 0 a une valeur par défaut, donc useCounter() sans argument démarre à zéro.
Utiliser un composable dans un composant
Dans un composant, tu importes la fonction et tu l'appelles dans <script setup>, puis tu déballes les valeurs renvoyées par déstructuration :
1<template>
2 <div style="background: #0a0e27; color: #00ff88; padding: 1rem;">
3 <h3>Module counter: {{ count }}</h3>
4 <button @click="increment">+1</button>
5 <button @click="decrement">-1</button>
6 <button @click="reset">Reset</button>
7 </div>
8</template>
9
10<script setup>
11import { useCounter } from './composables/useCounter'
12
13// Déstructure les valeurs renvoyées
14const { count, increment, decrement, reset } = useCounter(10)
15</script>Chaque composant qui utilise useCounter() obtient sa propre instance indépendante de l'état. Modifier count dans un composant n'affecte pas les autres. Appelle les composables de façon synchrone, au niveau supérieur de <script setup>, et non par exemple dans un gestionnaire de clic - c'est seulement ainsi que Vue les rattache au composant.
L'organisation des fichiers
Les composables sont stockés dans un dossier dédié, séparé des composants qui les utilisent :
1src/
2├── composables/
3│ ├── useTimer.js
4│ ├── useCounter.js
5│ ├── useFetch.js
6│ └── useMarsData.js
7├── components/
8│ ├── StatusBar.vue
9│ └── ModuleCard.vue
10└── App.vueLe plus souvent, c'est src/composables/ - Nuxt utilise aussi ce nom de dossier. Tu croiseras parfois src/hooks/, un nom emprunté à React. Vue n'impose rien ici ; l'important est que tout le projet s'en tienne à une seule convention.
Un composable pratique : useTimer
Le minuteur de mission est un composable typique utilisé dans de nombreux composants de la station, car il réunit un état, une valeur calculée et des hooks du cycle de vie :
1// composables/useTimer.js
2import { ref, computed, onMounted, onUnmounted } from 'vue'
3
4export function useTimer() {
5 const seconds = ref(0)
6 let intervalId = null
7
8 const formatted = computed(() => {
9 const h = String(Math.floor(seconds.value / 3600)).padStart(2, '0')
10 const m = String(Math.floor((seconds.value % 3600) / 60)).padStart(2, '0')
11 const s = String(seconds.value % 60).padStart(2, '0')
12 return `${h}:${m}:${s}`
13 })
14
15 function start() {
16 intervalId = setInterval(() => {
17 seconds.value++
18 }, 1000)
19 }
20
21 function stop() {
22 if (intervalId) {
23 clearInterval(intervalId)
24 intervalId = null
25 }
26 }
27
28 // Gestion automatique du cycle de vie
29 onMounted(() => start())
30 onUnmounted(() => stop())
31
32 return { seconds, formatted, start, stop }
33}Les hooks appelés dans un composable se rattachent au composant qui a appelé useTimer(), donc l'horloge démarre avec lui et s'arrête à son démontage. La méthode padStart(2, '0') ajoute un zéro devant, pour que 7 secondes s'affichent 07. Fais attention à l'import : computed doit être importé depuis vue comme ref, sinon le code s'arrête sur une erreur. Un composant qui a besoin d'une horloge ne prend dans la fonction que ce dont il a besoin :
1<!-- Utilisation dans n'importe quel composant -->
2<template>
3 <p>Mission time: {{ formatted }}</p>
4</template>
5
6<script setup>
7import { useTimer } from './composables/useTimer'
8
9const { formatted } = useTimer()
10</script>Le composant ne sait rien de setInterval ni du nettoyage - il reçoit un temps prêt et formaté.
Composable : useFetch
La récupération de données est un autre modèle typique : trois conteneurs décrivent l'état de la requête, et une seule fonction l'exécute :
1// composables/useFetch.js
2import { ref } from 'vue'
3
4export function useFetch(url) {
5 const data = ref(null)
6 const error = ref(null)
7 const loading = ref(false)
8
9 async function fetchData() {
10 loading.value = true
11 error.value = null
12
13 try {
14 const response = await fetch(url)
15 data.value = await response.json()
16 } catch (err) {
17 error.value = err.message
18 } finally {
19 loading.value = false
20 }
21 }
22
23 return { data, error, loading, fetchData }
24}Le conteneur loading vaut true pendant toute la durée de la requête, error garde le message d'erreur, et le bloc finally coupe le chargement quel que soit le résultat. L'appel de useFetch() seul ne récupère rien - c'est fetchData() qui le fait. Dans le composant, on l'appelle dans onMounted, et le template choisit l'un des trois états avec les directives v-if, v-else-if et v-else :
1<template>
2 <div>
3 <p v-if="loading">Loading station data...</p>
4 <p v-else-if="error">Error: {{ error }}</p>
5 <ul v-else-if="data">
6 <li v-for="module in data" :key="module.id">{{ module.name }}</li>
7 </ul>
8 </div>
9</template>
10
11<script setup>
12import { onMounted } from 'vue'
13import { useFetch } from './composables/useFetch'
14
15const { data, error, loading, fetchData } = useFetch('/api/modules')
16
17onMounted(() => fetchData())
18</script>Le même useFetch gérera la liste des modules, la météo sur Mars ou un rapport d'équipage - seule l'adresse change.
Pourquoi utiliser des composables ?
| Sans composables | Avec composables |
|---|---|
| Copier le même code de timer dans 5 composants | Un seul useTimer(), importé partout |
| Un changement de logique = éditer 5 fichiers | Un changement dans un seul fichier |
| Tests difficiles (logique dans les composants) | Tests plus faciles (logique dans des fonctions séparées) |
| Composants remplis de code répétitif | Composants focalisés sur l'affichage |
Un composable qui n'utilise que ref et computed se teste en l'appelant directement dans un test, alors qu'un composable avec des hooks du cycle de vie a besoin d'un composant hôte - nous y reviendrons au Laboratoire Scientifique.
Le plan de construction de la station
Au NOVA LAB, nous construisons une application par composants dans un ordre fixe : nous planifions la structure des composants, nous créons les composants de base comme un bouton ou une carte, nous extrayons la logique dans des composables, nous assemblons les composants en vues, et pour finir nous ajoutons les hooks du cycle de vie et les styles. Je te recommande cet ordre car chaque étape s'appuie sur la précédente. Dans l'éditeur ci-dessous, le composant MarsStation utilise useMarsData et useTimer - ouvre leurs fichiers et retrouve-y les modèles de cette leçon.
Retiens ceci : un composable est un système partagé de la station - une fonction use... qui renvoie un état réactif et de la logique, et chaque module s'y branche de façon indépendante.
Code de cette leçon : App.vue
1<script setup>
2import { ref } from 'vue'
3import MarsStation from './MarsStation.vue'
4
5const stationName = ref('NOVA LAB Alpha')
6</script>
7
8<template>
9 <div class="composables-lab">
10 <div class="header">
11 <h1>Composables - NOVA LAB</h1>
12 <p class="subtitle">Réutiliser la logique grâce aux composables</p>
13 </div>
14
15 <div class="info-panel">
16 <h3>Que sont les composables ?</h3>
17 <p>Un <strong>composable</strong> est une fonction qui commence par <strong>use</strong> (par ex. useMarsData)</p>
18 <p>Il encapsule un état réactif (ref) et de la logique que l'on peut partager entre composants</p>
19 <p>Structure : export function useXxx() { const state = ref(...); return { state } }</p>
20 </div>
21
22 <MarsStation :station-name="stationName" />
23 </div>
24</template>
25
26<style scoped>
27.composables-lab {
28 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
29 min-height: 100vh;
30 padding: 2rem;
31 color: #ffffff;
32 font-family: 'Courier New', monospace;
33}
34
35.header {
36 text-align: center;
37 margin-bottom: 2rem;
38 padding-bottom: 1rem;
39 border-bottom: 2px solid #00ff88;
40}
41
42.header h1 {
43 font-size: 2rem;
44 color: #00ff88;
45 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
46 margin: 0;
47}
48
49.subtitle {
50 color: #00b4d8;
51 font-size: 1.1rem;
52 margin-top: 0.5rem;
53}
54
55.info-panel {
56 background: rgba(0, 180, 216, 0.1);
57 border: 2px solid #00b4d8;
58 border-radius: 10px;
59 padding: 1.5rem;
60 margin-bottom: 2rem;
61}
62
63.info-panel h3 {
64 color: #00b4d8;
65 margin-top: 0;
66}
67
68.info-panel strong {
69 color: #00ff88;
70}
71</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. Qu'est-ce qu'un composable en Vue 3 ?
2. Comment nommer un composable selon les conventions de Vue ?
Voici 2 des 8 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Crée un composable useMarsWeather avec des données réactives et une fonction de récupération.
- Clique dans l'ordre
Range les éléments de la structure d'un composable dans l'ordre :
- Tri vertical
Range les étapes pour utiliser un composable dans un composant :
- Éditeur de code
Importe et utilise le composable useMarsWeather dans un composant WeatherPanel.vue.
- Tri vertical
Range les étapes de création et de test d'un composable, du fichier à l'utilisation :
- Clique dans l'ordre
Range les étapes, de la création à l'utilisation d'un composable :
- Éditeur de code
Crée un MarsColony avec des composants HabitatModule, LabModule et StorageModule imbriqués.
- Tri vertical
Range le processus, de la création à l'affichage d'un composant :
- Tri vertical
Range les hooks dans l'ordre où ils sont appelés lors du montage d'un composant :
- Éditeur de code
Crée un ensemble de composants : MarsButton et MarsCard avec des styles appropriés.
- Clique dans l'ordre
Range la syntaxe pour utiliser v-bind() dans la section style :
- Tri vertical
Range les étapes pour ajouter un lifecycle hook à un composant :
- Tri horizontal
Range la syntaxe pour utiliser une classe de CSS Module dans le template :
- Éditeur de code
Utilise le composable useMarsData avec onMounted pour initialiser les données dans le composant Dashboard.
- Tri vertical
Range les étapes de construction d'une application à partir de composants :
- Tri horizontal
Range l'import correct de ref et onMounted depuis Vue :
- Clique dans l'ordre
Range un composable avec onMounted :
- Tri vertical
Range les méthodes de style de la portée la plus large à la plus étroite :
- Tri horizontal
Range la syntaxe correcte pour utiliser onMounted :