Cours Vue.js · Module 14: Directives et plugins
Composants renderless et patterns fonctionnels
Dans cette leçon6
Au Centre de commandement de NOVA LAB, tous les modules d'interface n'ont pas besoin d'une représentation visuelle. Certains sont uniquement responsables de la logique - ce sont les composants renderless. Explorons les patterns avancés de composants Vue.
Qu'est-ce qu'un composant renderless ?
Un composant renderless est un composant qui ne rend pas son propre HTML - à la place, il expose sa logique à travers un slot. Le composant parent définit toute l'apparence visuelle.
1<script setup>
2import { ref, onMounted, onUnmounted } from 'vue'
3
4// Composant renderless - logique d'horloge
5const now = ref(new Date())
6let timer = null
7
8onMounted(() => {
9 timer = setInterval(() => {
10 now.value = new Date()
11 }, 1000)
12})
13
14onUnmounted(() => clearInterval(timer))
15
16defineExpose({ now })
17</script>
18
19<template>
20 <slot :now="now" :formatted="now.toLocaleTimeString('fr-FR')" />
21</template>Utilisation :
1<template>
2 <Clock v-slot="{ formatted }">
3 <div class="station-clock">{{ formatted }}</div>
4 </Clock>
5</template>Le composant Clock n'a pas de HTML propre - il transmet les données via un scoped slot, et le parent décide de l'apparence.
Pattern renderless avec logique de bascule
Un pattern populaire - un composant qui gère un état de bascule (toggle) :
1<!-- Toggle.vue -->
2<script setup>
3import { ref } from 'vue'
4
5const isOn = ref(false)
6
7function toggle() {
8 isOn.value = !isOn.value
9}
10
11function setOn() { isOn.value = true }
12function setOff() { isOn.value = false }
13</script>
14
15<template>
16 <slot :isOn="isOn" :toggle="toggle" :setOn="setOn" :setOff="setOff" />
17</template>Utilisation dans différents contextes :
1<template>
2 <!-- Comme interrupteur -->
3 <Toggle v-slot="{ isOn, toggle }">
4 <button @click="toggle">
5 System: {{ isOn ? 'ACTIVE' : 'OFFLINE' }}
6 </button>
7 </Toggle>
8
9 <!-- Comme panneau dépliable -->
10 <Toggle v-slot="{ isOn, toggle }">
11 <div>
12 <h3 @click="toggle">Diagnostics ▼</h3>
13 <div v-if="isOn">Détails du système...</div>
14 </div>
15 </Toggle>
16</template>Composants fonctionnels
Dans Vue 3, chaque composant avec <script setup> est déjà optimisé. Mais tu peux aussi créer de simples composants fonctionnels sous forme de fonctions de rendu (render functions) :
1import { h } from 'vue'
2
3// Composant fonctionnel - pas d'état, uniquement des props
4function StationBadge(props) {
5 return h('span', {
6 class: 'badge',
7 style: {
8 color: props.color || '#00ff88',
9 border: '1px solid ' + (props.color || '#00ff88'),
10 padding: '4px 12px',
11 borderRadius: '12px',
12 fontSize: '12px'
13 }
14 }, props.label)
15}
16
17StationBadge.props = ['label', 'color']Utilisation dans un template :
1<template>
2 <StationBadge label="ONLINE" color="#2ecc71" />
3 <StationBadge label="ALERT" color="#e74c3c" />
4</template>Fonctions de rendu avec h()
La fonction h() (hyperscript) crée un VNode - un élément du DOM virtuel :
1import { h } from 'vue'
2
3// h(tag, props, children)
4h('div', { class: 'panel' }, [
5 h('h2', null, 'Titre'),
6 h('p', null, 'Contenu du panneau')
7])Un composant avec une fonction de rendu au lieu d'un template :
1import { h, ref, defineComponent } from 'vue'
2
3const Counter = defineComponent({
4 setup() {
5 const count = ref(0)
6
7 return () => h('div', { class: 'counter' }, [
8 h('p', null, 'Counter: ' + count.value),
9 h('button', {
10 onClick: () => count.value++
11 }, '+1')
12 ])
13 }
14})Higher-Order Components (HOC)
Un HOC est une fonction qui prend un composant et retourne un nouveau composant avec des fonctionnalités ajoutées :
1import { h, ref, onMounted } from 'vue'
2
3function withLoading(WrappedComponent) {
4 return defineComponent({
5 setup(props, { attrs }) {
6 const loading = ref(true)
7
8 onMounted(() => {
9 setTimeout(() => {
10 loading.value = false
11 }, 1000)
12 })
13
14 return () => {
15 if (loading.value) {
16 return h('div', { class: 'loading' }, 'Chargement du module...')
17 }
18 return h(WrappedComponent, { ...attrs })
19 }
20 }
21 })
22}Utilisation :
1const MissionPanelWithLoading = withLoading(MissionPanel)Quand utiliser ces patterns ?
| Pattern | Quand l'utiliser |
|---|---|
| Composant renderless | Logique réutilisable avec une UI flexible |
| Composant fonctionnel | Éléments de présentation simples et sans état |
| Fonction de rendu (h()) | Génération dynamique de structures DOM |
| HOC | Ajouter un comportement à des composants existants |
Les composants renderless et les scoped slots sont la façon idiomatique de Vue de séparer la logique de la présentation - particulièrement utile dans des systèmes aussi complexes que le Centre de commandement de NOVA LAB.
Code de cette leçon : App.vue
1<script setup>
2import { ref, h, defineComponent } from 'vue'
3
4// Composant Toggle renderless
5const Toggle = defineComponent({
6 setup(_, { slots }) {
7 const isOn = ref(false)
8 function toggle() { isOn.value = !isOn.value }
9 function setOn() { isOn.value = true }
10 function setOff() { isOn.value = false }
11
12 return () => slots.default?.({
13 isOn: isOn.value,
14 toggle,
15 setOn,
16 setOff
17 })
18 }
19})
20
21// Composant fonctionnel
22function StationBadge(props) {
23 return h('span', {
24 style: {
25 color: props.color || '#00ff88',
26 border: '1px solid ' + (props.color || '#00ff88'),
27 padding: '4px 12px',
28 borderRadius: '12px',
29 fontSize: '12px',
30 display: 'inline-block'
31 }
32 }, props.label)
33}
34StationBadge.props = ['label', 'color']
35
36// Counter avec fonction de rendu
37const Counter = defineComponent({
38 setup() {
39 const count = ref(0)
40 return () => h('div', { class: 'counter-box' }, [
41 h('span', null, 'Counter: ' + count.value),
42 h('button', {
43 onClick: () => count.value++,
44 class: 'btn small',
45 style: 'margin-left: 10px'
46 }, '+1'),
47 h('button', {
48 onClick: () => count.value = 0,
49 class: 'btn small reset',
50 style: 'margin-left: 6px'
51 }, 'Reset')
52 ])
53 }
54})
55</script>
56
57<template>
58 <div class="nova-lab">
59 <h1>Composants renderless et patterns fonctionnels</h1>
60
61 <section class="section">
62 <h2>Renderless Toggle</h2>
63 <Toggle v-slot="{ isOn, toggle }">
64 <div>
65 <button @click="toggle" :class="['btn', { active: isOn }]">
66 System: {{ isOn ? 'ACTIVE' : 'OFF' }}
67 </button>
68 <div v-if="isOn" class="panel">
69 <p>Systèmes de diagnostic en cours d'exécution.</p>
70 <p>Température : 22.5°C | Pression : 1.01 atm</p>
71 </div>
72 </div>
73 </Toggle>
74 </section>
75
76 <section class="section">
77 <h2>Renderless Toggle - autre usage</h2>
78 <Toggle v-slot="{ isOn, toggle }">
79 <div>
80 <h3 @click="toggle" class="expandable">
81 {{ isOn ? '▼' : '▶' }} Journaux de la station
82 </h3>
83 <ul v-if="isOn" class="log-list">
84 <li>[09:15] Système de navigation OK</li>
85 <li>[09:16] Scan du secteur terminé</li>
86 <li>[09:17] Synchronisation des données</li>
87 </ul>
88 </div>
89 </Toggle>
90 </section>
91
92 <section class="section">
93 <h2>Composant fonctionnel - StationBadge</h2>
94 <div class="badge-row">
95 <StationBadge label="ONLINE" color="#2ecc71" />
96 <StationBadge label="ALERT" color="#e74c3c" />
97 <StationBadge label="STANDBY" color="#f39c12" />
98 <StationBadge label="NOVA LAB" color="#00b4d8" />
99 </div>
100 </section>
101
102 <section class="section">
103 <h2>Fonction de rendu - Counter</h2>
104 <Counter />
105 </section>
106 </div>
107</template>
108
109<style scoped>
110.nova-lab {
111 background: #0a0e27;
112 color: #c0c0c0;
113 min-height: 100vh;
114 padding: 20px;
115 font-family: monospace;
116}
117h1 { color: #00ff88; margin-bottom: 20px; }
118h2 { color: #00b4d8; font-size: 16px; margin-bottom: 10px; }
119h3 { color: #00ff88; margin: 0; }
120.section {
121 background: rgba(0, 180, 216, 0.05);
122 border: 1px solid rgba(0, 180, 216, 0.2);
123 padding: 15px;
124 border-radius: 8px;
125 margin-bottom: 15px;
126}
127.btn {
128 background: rgba(0,255,136,0.15);
129 color: #00ff88;
130 border: 1px solid #00ff88;
131 padding: 8px 16px;
132 border-radius: 6px;
133 cursor: pointer;
134 font-family: monospace;
135}
136.btn.active { background: #00ff88; color: #0a0e27; }
137.btn.small { font-size: 12px; padding: 4px 10px; }
138.btn.reset { color: #e74c3c; border-color: #e74c3c; }
139.panel {
140 margin-top: 10px;
141 padding: 10px;
142 background: rgba(0,255,136,0.05);
143 border: 1px solid rgba(0,255,136,0.2);
144 border-radius: 6px;
145}
146.panel p { margin: 4px 0; font-size: 13px; }
147.expandable { cursor: pointer; user-select: none; }
148.expandable:hover { color: #00b4d8; }
149.log-list {
150 list-style: none;
151 padding: 0;
152 margin: 8px 0 0;
153}
154.log-list li {
155 padding: 4px 0;
156 font-size: 13px;
157 color: #888;
158 border-bottom: 1px solid rgba(0,180,216,0.1);
159}
160.badge-row { display: flex; gap: 10px; flex-wrap: wrap; }
161.counter-box {
162 display: flex;
163 align-items: center;
164 padding: 10px;
165 background: rgba(0,255,136,0.05);
166 border-radius: 6px;
167}
168</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 composant renderless dans Vue ?
2. Comment un composant renderless transmet-il des données au composant parent ?
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
Implémente un composant Toggle avec un ref isOn, une fonction toggle et un scoped slot.
- Tri vertical
Ordonne les étapes de création d'un composant renderless dans le bon ordre :
- Clique dans l'ordre
Ordonne le bon ordre des arguments de la fonction h() :
- Éditeur de code
Implémente StationBadge(props) comme une fonction retournant h('span', ...) avec props.label et props.color.
- Tri horizontal
Ordonne l'import correct de la fonction h depuis Vue :
- Éditeur de code
Implémente un Counter avec un ref count et une fonction de rendu retournant h() avec des boutons +1 et Reset.
- Tri vertical
Ordonne les patterns du plus simple au plus avancé :