Vue.js course · Module 14: Directives & Plugins
Renderless Components and Functional Patterns
In this lesson6
In the NOVA LAB Command Center, not all interface modules need a visual representation. Some are responsible solely for logic - these are renderless components. Let's explore advanced Vue component patterns.
What Is a Renderless Component?
A renderless component is a component that does not render its own HTML - instead, it exposes logic through a slot. The parent component defines all the visual appearance.
1<script setup>
2import { ref, onMounted, onUnmounted } from 'vue'
3
4// Renderless component - clock logic
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('en-US')" />
21</template>Usage:
1<template>
2 <Clock v-slot="{ formatted }">
3 <div class="station-clock">{{ formatted }}</div>
4 </Clock>
5</template>The Clock component has no HTML of its own - it passes data through a scoped slot, and the parent decides on the appearance.
Renderless Pattern with Toggle Logic
A popular pattern - a component managing toggle state:
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>Usage in different contexts:
1<template>
2 <!-- As a switch -->
3 <Toggle v-slot="{ isOn, toggle }">
4 <button @click="toggle">
5 System: {{ isOn ? 'ACTIVE' : 'OFFLINE' }}
6 </button>
7 </Toggle>
8
9 <!-- As an expandable panel -->
10 <Toggle v-slot="{ isOn, toggle }">
11 <div>
12 <h3 @click="toggle">Diagnostics ▼</h3>
13 <div v-if="isOn">System details...</div>
14 </div>
15 </Toggle>
16</template>Functional Components
In Vue 3, every component with <script setup> is already optimized. But you can also create simple functional components as plain render functions:
1import { h } from 'vue'
2
3// Functional component - no state, only 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']Usage in a template:
1<template>
2 <StationBadge label="ONLINE" color="#2ecc71" />
3 <StationBadge label="ALERT" color="#e74c3c" />
4</template>Render Functions with h()
The h() function (hyperscript) creates a VNode - a virtual DOM element:
1import { h } from 'vue'
2
3// h(tag, props, children)
4h('div', { class: 'panel' }, [
5 h('h2', null, 'Title'),
6 h('p', null, 'Panel content')
7])A component with a render function instead of a 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)
An HOC is a function that takes a component and returns a new component with added functionality:
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' }, 'Loading module...')
17 }
18 return h(WrappedComponent, { ...attrs })
19 }
20 }
21 })
22}Usage:
1const MissionPanelWithLoading = withLoading(MissionPanel)When to Use These Patterns?
| Pattern | When to Use |
|---|---|
| Renderless component | Reusable logic with flexible UI |
| Functional component | Simple, stateless presentational elements |
| Render function (h()) | Dynamic DOM structure generation |
| HOC | Adding behavior to existing components |
Renderless components and scoped slots are Vue's idiomatic way of separating logic from presentation - especially useful in systems as complex as the NOVA LAB Command Center.
Code for this lesson: App.vue
1<script setup>
2import { ref, h, defineComponent } from 'vue'
3
4// Renderless Toggle component
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// Functional component
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 with render function
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>Renderless Components i Functional Patterns</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>Diagnostic systems running.</p>
70 <p>Temperature: 22.5°C | Pressure: 1.01 atm</p>
71 </div>
72 </div>
73 </Toggle>
74 </section>
75
76 <section class="section">
77 <h2>Renderless Toggle - another use</h2>
78 <Toggle v-slot="{ isOn, toggle }">
79 <div>
80 <h3 @click="toggle" class="expandable">
81 {{ isOn ? '▼' : '▶' }} Station logs
82 </h3>
83 <ul v-if="isOn" class="log-list">
84 <li>[09:15] Navigation system OK</li>
85 <li>[09:16] Sector scan completed</li>
86 <li>[09:17] Data synchronization</li>
87 </ul>
88 </div>
89 </Toggle>
90 </section>
91
92 <section class="section">
93 <h2>Functional Component - 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>Render Function - 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>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is a renderless component in Vue?
2. How does a renderless component pass data to the parent component?
These are 2 of 4 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Implement a Toggle component with ref isOn, a toggle function, and a scoped slot.
- Vertical ordering
Arrange the steps for creating a renderless component in the correct order:
- Click in order
Arrange the correct order of arguments for the h() function:
- Code editor
Implement StationBadge(props) as a function returning h('span', ...) with props.label and props.color.
- Horizontal ordering
Arrange the correct import of the h function from Vue:
- Code editor
Implement a Counter with ref count and a render function returning h() with +1 and Reset buttons.
- Vertical ordering
Arrange the patterns from simplest to most advanced: