Vue.js course · Module 14: Directives & Plugins

Renderless Components and Functional Patterns

4 min read
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?

PatternWhen to Use
Renderless componentReusable logic with flexible UI
Functional componentSimple, stateless presentational elements
Render function (h())Dynamic DOM structure generation
HOCAdding 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. 1. What is a renderless component in Vue?

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

Useful articles