Cours Vue.js · Module 14: Directives et plugins

Composants renderless et patterns fonctionnels

4 min de lecture
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 ?

PatternQuand l'utiliser
Composant renderlessLogique 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
HOCAjouter 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. 1. Qu'est-ce qu'un composant renderless dans Vue ?

  2. 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é :