Vue.js course Β· Module 8: Slots & Dynamic Components
Dynamic Components
In this lesson6
The NOVA LAB module transport system lets you connect different panels to the same station socket - the oxygen panel, the water panel, the power panel. The operator switches them with a single button. You could write a chain of v-if and v-else-if with a separate condition for each panel, but every new panel would mean editing the template, and the chain would grow forever. Vue has a mechanism for this called dynamic components - a special <component> element with an :is attribute that renders whichever component you point it at.
Basic Syntax
The implementation takes four steps: you import all the panels, create a variable pointing at the active panel, render it through <component :is> and add buttons that change that variable. To store the component we use shallowRef, a ref that reacts only when the whole value is replaced:
1<script setup>
2import { ref, shallowRef } from 'vue'
3import OxygenPanel from './OxygenPanel.vue'
4import WaterPanel from './WaterPanel.vue'
5import SolarPanel from './SolarPanel.vue'
6
7const tabs = { OxygenPanel, WaterPanel, SolarPanel }
8const currentTab = shallowRef(OxygenPanel)
9</script>
10
11<template>
12 <div class="station">
13 <nav>
14 <button
15 v-for="(comp, name) in tabs"
16 :key="name"
17 :class="{ active: currentTab === comp }"
18 @click="currentTab = comp"
19 >
20 {{ name }}
21 </button>
22 </nav>
23
24 <component :is="currentTab" />
25 </div>
26</template>The v-for loop over the tabs object gives the value, which is the component, and the key, which is its name on the button. A click replaces currentTab, and <component> renders the new panel in the same place; the active class highlights the active button. We use shallowRef instead of ref because we don't need deep reactivity on the component object - Vue would even warn that it is needlessly turning a component into a reactive object.
What :is Accepts
You can pass an imported component object to :is, as above, or a string: the name of a component registered globally with app.component(), or the name of a plain HTML tag. In the example below the isLink variable decides whether a link or a button is created:
1<template>
2 <!-- Imported component -->
3 <component :is="OxygenPanel" />
4
5 <!-- Name of a globally registered component -->
6 <component is="WaterPanel" />
7
8 <!-- Plain HTML tag -->
9 <component :is="isLink ? 'a' : 'button'">Open airlock</component>
10</template>In <script setup> imported components are variables, not registered names, so there you always pass the object through :is. A string only works for globally registered components.
KeepAlive
By default, switching components destroys the old one and creates a new one. The order of hooks is predictable: the old panel gets onBeforeUnmount, then onUnmounted, and the new one gets onMounted - from scratch, so a moved slider or typed text is lost. If you want to keep the state, wrap the component in <KeepAlive>:
1<template>
2 <KeepAlive>
3 <component :is="currentTab" />
4 </KeepAlive>
5</template>Now the component is cached - you will come back to it with its state preserved. The panel's element leaves the document, but the instance with all its state waits in memory. <KeepAlive> itself renders no element in the DOM.
KeepAlive - include and exclude
Not every panel is worth keeping in memory. The include and exclude attributes compare the component's name with a list, and max limits the cache size:
1<template>
2 <!-- Only these components -->
3 <KeepAlive :include="['OxygenPanel', 'WaterPanel']">
4 <component :is="currentTab" />
5 </KeepAlive>
6
7 <!-- All except these -->
8 <KeepAlive :exclude="['HeavyModule']">
9 <component :is="currentTab" />
10 </KeepAlive>
11
12 <!-- Cache limit (LRU) -->
13 <KeepAlive :max="5">
14 <component :is="currentTab" />
15 </KeepAlive>
16</template>Where does a component get its name? A file with <script setup> gets it automatically from the file name (since Vue 3.2.34), so OxygenPanel.vue is called OxygenPanel. The list can also be a comma-separated string or a regular expression. With max, the cache works as an LRU: when it runs out of room, Vue destroys the least recently used instance. A panel outside include behaves as if there were no KeepAlive.
Lifecycle Hooks with KeepAlive
Cached components have two extra hooks, because instead of being destroyed they are only detached:
1<script setup>
2import { onActivated, onDeactivated } from 'vue'
3
4onActivated(() => {
5 console.log('Panel activated (visible)')
6 // Refresh data, resume timers
7})
8
9onDeactivated(() => {
10 console.log('Panel deactivated (hidden)')
11 // Stop timers, save state
12})
13</script>On the first render the panel gets onMounted, and right after it onActivated, because this hook also runs on mount. When you switch, the old panel gets onDeactivated instead of onUnmounted, and the new one gets onActivated. When you come back there is no onMounted any more - only onActivated.
Props on Dynamic Components
You can pass props and listen to events as with a regular component; they reach whichever panel is currently rendered:
1<template>
2 <component
3 :is="currentTab"
4 :station="stationData"
5 @alert="handleAlert"
6 />
7</template>So when should you use <component :is> and when v-if? Two fixed variants are easily handled by v-if. When there are many components and you want to switch between them dynamically, <component :is> gives a cleaner template.
My advice: keep the panels in an array of { name, component } objects - you will generate the buttons with a single v-for and highlight the active one by comparing currentTab === tab.component. In the editor below, three panels are built exactly this way, and KeepAlive makes sure the oxygen level slider doesn't jump back to its starting value.
Remember: <component :is> is the station's universal socket - you plug in any panel, and KeepAlive keeps its state while you temporarily unplug it.
Code for this lesson: App.vue
1<script setup>
2import { shallowRef } from 'vue'
3import OxygenPanel from './OxygenPanel.vue'
4import WaterPanel from './WaterPanel.vue'
5import SolarPanel from './SolarPanel.vue'
6
7const tabs = [
8 { name: 'Oxygen', component: OxygenPanel },
9 { name: 'Water', component: WaterPanel },
10 { name: 'Solar', component: SolarPanel }
11]
12
13const currentTab = shallowRef(OxygenPanel)
14</script>
15
16<template>
17 <div class="nova-lab">
18 <h1>Dynamic Components</h1>
19
20 <nav class="tabs">
21 <button
22 v-for="tab in tabs"
23 :key="tab.name"
24 :class="{ active: currentTab === tab.component }"
25 @click="currentTab = tab.component"
26 >
27 {{ tab.name }}
28 </button>
29 </nav>
30
31 <KeepAlive>
32 <component :is="currentTab" />
33 </KeepAlive>
34 </div>
35</template>
36
37<style scoped>
38.nova-lab {
39 background: #0a0e27;
40 color: #00ff88;
41 min-height: 100vh;
42 padding: 20px;
43 font-family: monospace;
44}
45h1 { color: #00ff88; margin-bottom: 20px; }
46.tabs {
47 display: flex;
48 gap: 5px;
49 margin-bottom: 15px;
50}
51.tabs button {
52 background: rgba(0, 180, 216, 0.1);
53 border: 2px solid #00b4d8;
54 color: #00b4d8;
55 padding: 10px 20px;
56 cursor: pointer;
57 font-family: monospace;
58 font-weight: bold;
59}
60.tabs button.active {
61 background: #00b4d8;
62 color: #0a0e27;
63}
64</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. How do you render a dynamic component in Vue?
2. What can be passed to the :is attribute in <component>?
Hands-on tasks in the game
- Code editor
Create a tab system with <component :is> that switches between three components: OxygenPanel, WaterPanel, SolarPanel.
- Vertical ordering
Order the steps for implementing a dynamic component:
- Click in order
Arrange the code for a dynamic component with buttons:
- Code editor
Create a component with a list of tabs in an array and render the active component via <component :is>. Highlight the active tab.