Chapter 31: Vue Dynamic Components
Dynamic Components (<component :is=”…”>)
This is the Vue feature that lets you decide at runtime which component to render — instead of hard-coding the tag name in the template.
In simple words:
Instead of writing <UserProfile /> or <ProductCard /> directly, you write <component :is=”currentComponent” /> and then change currentComponent dynamically based on data, user action, route, permission, tab selection, etc.
This is extremely common in real applications — almost every medium/large Vue app uses dynamic components somewhere.
Why Do We Need Dynamic Components? Real-Life Situations
- Tab navigation / dashboard widgets Different tabs show completely different components
- Role-based UI (admin vs user vs guest sees different views)
- Lazy-loaded / async components (load heavy chart only when needed)
- Wizard / stepper forms (step 1 → step 2 → step 3 = different component)
- Content rendering from CMS / API (page builder style — JSON says which component to show)
- A/B testing or feature flags (show variant A or B)
- Polymorphic lists (render UserItem / ProductItem / OrderItem based on type)
Basic Syntax – The <component :is> Tag
|
0 1 2 3 4 5 6 |
<component :is="currentView" /> |
:is can be:
- A component name (string) — if globally registered
- A component definition (imported component object)
- A dynamic import (() => import(‘./SomeView.vue’))
- A resolved component from a map/object
Real, Complete Example – Tabbed Dashboard (Most Common Use Case)
File: src/views/DashboardView.vue
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
<template> <div class="dashboard"> <h1>My Dashboard</h1> <!-- Tab buttons --> <div class="tabs"> <button v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }" @click="activeTab = tab.name" > {{ tab.label }} </button> </div> <!-- Dynamic component – this is the magic --> <component :is="currentComponent" :data="tabData" @some-event="handleEvent" /> <!-- Loading state while component loads (optional) --> <div v-if="!currentComponent" class="loading"> Loading... </div> </div> </template> <script setup lang="ts"> import { ref, computed, defineAsyncComponent } from 'vue' // 1. Import or define components (can be sync or async) import Overview from '@/components/dashboard/Overview.vue' // Async / lazy-loaded components (great for performance) const Analytics = defineAsyncComponent(() => import('@/components/dashboard/Analytics.vue')) const Settings = defineAsyncComponent(() => import('@/components/dashboard/Settings.vue')) const Users = defineAsyncComponent(() => import('@/components/dashboard/Users.vue')) // 2. Tab configuration (very clean pattern) const tabs = [ { name: 'overview', label: 'Overview', component: Overview }, { name: 'analytics', label: 'Analytics', component: Analytics }, { name: 'users', label: 'Users', component: Users }, { name: 'settings', label: 'Settings', component: Settings } ] // 3. Reactive state const activeTab = ref('overview') // 4. Computed – resolves which component to show const currentComponent = computed(() => { const tab = tabs.find(t => t.name === activeTab.value) return tab?.component || null }) // Optional: pass data to dynamic component const tabData = computed(() => { return { activeTab: activeTab.value } // or fetch real data }) function handleEvent(payload: any) { console.log('Event from dynamic component:', payload) } </script> <style scoped> .dashboard { padding: 2rem; max-width: 1400px; margin: 0 auto; } .tabs { display: flex; gap: 0.5rem; margin-bottom: 2rem; border-bottom: 1px solid #e5e7eb; } .tabs button { padding: 0.8rem 1.5rem; background: none; border: none; border-bottom: 3px solid transparent; font-weight: 500; cursor: pointer; transition: all 0.2s; } .tabs .active { border-bottom-color: #3b82f6; color: #3b82f6; } .loading { text-align: center; padding: 4rem; color: #6b7280; } </style> |
Different Ways to Use :is (All Valid in 2026)
-
Static string (needs global registration)
vue0123456<component :is="'UserProfile'" /> -
Imported component directly
vue01234567import Profile from './Profile.vue'<component :is="Profile" /> -
Async / lazy component (best for performance)
vue01234567const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))<component :is="showChart ? HeavyChart : null" /> -
Object map / dictionary (very clean for tabs)
JavaScript0123456789101112const views = {home: HomeView,profile: ProfileView,settings: SettingsView}<component :is="views[activeView]" /> -
Dynamic import based on string (CMS / config driven)
JavaScript01234567const componentName = 'SomeWidget' // from API<component :is="() => import(`@/components/widgets/{componentName}.vue`)" />
Important Gotchas & Pro Tips (2026 Edition)
-
Always use :key when switching dynamic components in v-if / v-show / lists
vue0123456<component :is="current" :key="activeTab" />→ Forces Vue to destroy + recreate when tab changes → prevents state leakage
-
Use <KeepAlive> when you want to preserve state across switches
vue012345678<KeepAlive><component :is="currentView" /></KeepAlive>→ Amazing for tabs — keeps form data, scroll position, etc.
-
Error handling for failed async components
vue0123456789101112const MyComponent = defineAsyncComponent({loader: () => import('./MyComp.vue'),errorComponent: ErrorView,loadingComponent: LoadingSpinner,delay: 200,timeout: 3000}) -
TypeScript — works beautifully with defineAsyncComponent<…>()
Summary Table – Dynamic Components in Practice
| Use Case | Recommended :is Pattern | Bonus Tip / Pattern |
|---|---|---|
| Tab / wizard navigation | Map object + computed | + <KeepAlive> |
| Role/permission-based UI | views[role] or ternary | Combine with v-if for fallback |
| Lazy heavy components | defineAsyncComponent(() => import(…)) | + loading/error states |
| CMS / config-driven rendering | Dynamic import from string | Security: validate allowed components |
| Polymorphic list items | <component :is=”getComponentType(item)” /> | Often inside v-for |
Your Mini Homework
- Create 3 small components: OverviewTab.vue, StatsTab.vue, ProfileTab.vue
- Build a tabbed interface exactly like the example above
- Add <KeepAlive> → switch tabs → see that form inputs / scroll position are preserved
Any part still fuzzy? Want to see:
- Dynamic components + <transition> for smooth tab animations?
- Real CMS-style rendering from JSON?
- Error boundary around async dynamic components?
- Dynamic components inside v-for (polymorphic list)?
Just tell me — we’ll build the next powerful example together 🚀
Happy dynamic-component-ing from Hyderabad! 💙
