Vue.js: Dynamic & Async Components

Last updated: 2026-10-20

Dynamic components let you render different components from a single <component> tag—switching tabs, modals, and route views based on data. Async components let you load components on demand—shipping only the code the first screen needs, which improves performance.

Vue 3's experimental <Suspense> component is purpose-built for the loading state of async components, so you can stop hand-writing v-if="loading". This lesson covers these 3 core APIs.

1. What You'll Learn


2. The 5-v-if Nightmare of Tab Switching

(1) The Pain: 5 Tabs, 10 Nested v-ifs

Alice built an admin dashboard with tab switching:

Product manager Charlie:

"Alice, we're adding 5 more tabs. 10 lines of v-if-else is completely unmaintainable. Use dynamic components."

(2) The Vue Dynamic Component Fix: One <component> for 5 Tabs

VUE
<!-- ✅ Correct version: a single <component> switches them -->
<template>
  <div class="tabs">
    <button 
      v-for="tab in tabs" 
      :key="tab.name"
      :class="{ active: currentTab === tab.name }"
      @click="currentTab = tab.name"
    >
      {{ tab.label }}
    </button>
    
    <!-- ✅ One <component> tag switches dynamically -->
    <component :is="currentTabComponent" />
  </div>
</template>

<script setup>
const { ref, computed, defineAsyncComponent } = Vue

// Synchronous components
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'

// Async component (loaded on demand)
const SettingsTab = defineAsyncComponent(() => import('./tabs/SettingsTab.vue'))

const tabs = [
  { name: 'home', label: 'Home' },
  { name: 'profile', label: 'Profile' },
  { name: 'settings', label: 'Settings' }
]

const currentTab = ref('home')

// Computed: decide which component to render based on currentTab
const currentTabComponent = computed(() => {
  return { home: HomeTab, profile: ProfileTab, settings: SettingsTab }[currentTab.value]
})
</script>

(3) The Payoff

After switching to dynamic components:


3. <component :is="..."> Basics

(1) 5 Ways to Write :is

VUE
<!-- Form 1: component name string (globally registered) -->
<component :is="'HomeTab'" />

<!-- Form 2: component object (most common) -->
<component :is="HomeTab" />

<!-- Form 3: dynamic computed property -->
<component :is="currentTabComponent" />

<!-- Form 4: async component object -->
<component :is="asyncComponent" />

<!-- Form 5: inline component object -->
<component :is="{ template: '<div>Inline</div>' }" />

(2) Complete Example

VUE
<!-- TabContainer.vue -->
<template>
  <div>
    <button 
      v-for="tab in tabs" 
      :key="tab.name"
      @click="currentTab = tab.name"
    >
      {{ tab.label }}
    </button>
    
    <!-- Dynamic component -->
    <component :is="currentTabComponent" />
  </div>
</template>

<script setup>
const { ref, computed } = Vue
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'
import SettingsTab from './tabs/SettingsTab.vue'

const tabs = [
  { name: 'home', label: 'Home' },
  { name: 'profile', label: 'Profile' },
  { name: 'settings', label: 'Settings' }
]

const currentTab = ref('home')

const currentTabComponent = computed(() => ({
  home: HomeTab,
  profile: ProfileTab,
  settings: SettingsTab
}[currentTab.value]))
</script>

(3) 5 Common Use Cases

Use case Dynamic component
Tab switching ✅
Modal/Dialog ✅
Multi-step forms ✅
Route views ✅ (<router-view> uses component internally)
Theme switching (different layouts) ✅

▶ Example: One <component :is> switching 3 panels

▶ Try it Yourself

Output:

TEXT
Initially the "Home" button is highlighted green with white text, and the dashed box below reads "Home: 3 todos today, 5 unread messages".
Click "Profile" → the highlight moves, and the whole panel is replaced by "Profile: Alice / admin / signed in for 12 days".
Click "Settings" → it swaps again; all three panels share one `<component>` slot, so the old component is destroyed and the new one is created on every switch.

4. Async Components with defineAsyncComponent

(1) Why Async Components?

A normal component loads all of its code immediately at import time. Large components (rich text editors, chart libraries) aren't needed for the first screen but still block loading. Async components load on demand, making the first screen faster.

VUE
<script setup>
const { defineAsyncComponent } = Vue
import LoadingSpinner from './LoadingSpinner.vue'

// Async component: ChartEditor's code loads only when it's used
const ChartEditor = defineAsyncComponent({
  // 1. Loader function
  loader: () => import('./ChartEditor.vue'),
  
  // 2. Shown while loading
  loadingComponent: LoadingSpinner,
  
  // 3. Shown if loading fails
  errorComponent: ErrorMessage, // requires import ErrorMessage from './ErrorMessage.vue'
  
  // 4. Load delay (avoids flicker)
  delay: 200
})
</script>

<template>
  <ChartEditor v-if="showEditor" />
</template>

(2) Shorthand

VUE
<script setup>
const { defineAsyncComponent } = Vue

// ✅ Shorthand: loader only
const ChartEditor = defineAsyncComponent(() => import('./ChartEditor.vue'))
</script>

(3) 5 Common Async Component Scenarios

Scenario Async load
Rich text editor ✅
Chart libraries (ECharts/D3) ✅
Large forms (100+ fields) ✅
Modal dialogs ✅
Lazy-loaded routes ✅ (Vue Router does this automatically)

5. Handling Async Components with <Suspense>

(1) What Is Suspense?

Suspense is Vue 3's loading coordinator for async components—it handles the loading and error states of async components for you. No more hand-written v-if="loading".

(2) 2 Slots

(3) 5 Advantages

Advantage Description
Concise No hand-written loading state
Unified Multiple async components handled together
Nestable Nested Suspense is supported
Error handling Works with onErrorCaptured
SSR friendly The server waits automatically

6. Dynamic Components + keep-alive Caching

(1) Default Behavior: Switching Destroys and Recreates

(2) keep-alive Caching (Avoid Recreating)

(3) keep-alive's 3 Config Options

▶ Example: Dynamic tabs + keep-alive caching

▶ Expand runnable example (69 lines)
▶ Try it Yourself

Output:

TEXT
Three tab buttons render, with the active one highlighted in green.
The panel below starts on the Home tab and shows a counter button plus a grey hint.
Clicking the counter to 3, switching to Profile, and coming back to Home still shows Count: 3, while the console logs mounted once followed by activated / deactivated instead of destroy events.
TEXT
[Home] [Profile] [Settings]        ← active tab highlighted in green
┌──────────────────────────────────────┐
│ Home                                 │
│ [Count: 3]                           │
│ Count is kept after a switch (cache) │
└──────────────────────────────────────┘

console:
🏠 Home mounted
🏠 Home activated
🏠 Home deactivated        ← switch to Profile
👤 Profile mounted
👤 Profile activated
👤 Profile deactivated    ← switch back to Home
🏠 Home activated          ← no second "mounted"

7. Putting It Together: Async Loading with Suspense

▶ Example: Handling async loading with Suspense

▶ Try it Yourself
⚠️ Real projects use defineAsyncComponent(() => import('./Chart.vue')). Here async setup() is used to simulate the delay.

Output:

TEXT
The page shows a button labelled Load async component.
Clicking it renders the amber ⏳ Loading...
fallback for roughly 1.5s, then replaces it with a green panel containing AsyncHeavy and Loaded at: <current time>.
Clicking again unmounts the component.
TEXT
[Load async component]

0.0s – 1.5s   ⏳ Loading...            (amber fallback, #fef3c7)
after 1.5s    AsyncHeavy
              Loaded at: 10:24:31      (green panel, #ecfdf5)

button label becomes [Unload async component]
click it → the panel disappears

(1) 📊 5 forms of :is, compared

Form Example Best for
String <component :is="'HomeTab'"> Global registration (not recommended)
Component object <component :is="HomeTab"> Static import (most common)
Async component object <component :is="AsyncComponent"> On-demand loading
Computed property <component :is="currentTabComponent"> Dynamic switching
Inline object <component :is="{ template: '...' }"> Simple cases

(2) 📊 5 Common Mistakes at a Glance

Mistake Symptom Fix
:is is a string but not globally registered Unknown component warning Use a component object or register it first
Async component without a fallback Blank screen while loading Wrap it in Suspense
Component switching reloads data Poor performance Cache with keep-alive
Async component errors unhandled Silent failure Pair with onErrorCaptured
Dynamic component without :key State gets mixed up Add :key="tab.name"

(3) 📊 5 Performance Comparisons

Pattern First screen Switching Best for
Direct import Slow Reloads Small components
Dynamic import Fast Slow on first switch Large components
Suspense Fast Smooth UX-first
keep-alive Fast 0 reloads Frequent switching
Async + keep-alive ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Best practice

❓ FAQ

Q What's the difference between dynamic components and v-if?
A With v-if, 5 conditions mean 5 branches and all components are loaded. A dynamic component is one <component> tag that switches via :is—much more concise. Async components can additionally load on demand.
Q Does defineAsyncComponent have to be at the top level of setup?
A Yes. defineAsyncComponent(() => import('...')) must be called at the top level of <script setup> (not inside a function).
Q Is Suspense a stable feature?
A It has been stable since Vue 3.2. You can use it with confidence, but in production it's worth testing your fallback behavior thoroughly.
Q Can dynamic components receive props?
A Yes. Just bind them: <component :is="Comp" :prop1="x" :prop2="y" />.
Q How do I catch async component failures?
A Use the errorComponent option plus onErrorCaptured: { loader, errorComponent: ErrorComp }. Inside Suspense you can wrap with onErrorCaptured.
Q Is keep-alive cached data persisted?
A Data is preserved while cached (not destroyed). But a page refresh or route change clears the cache. Use Pinia if you need persistence.
Q How do defineAsyncComponent and Vue Router lazy loading relate?
A Vue Router implements lazy-loaded routes with defineAsyncComponent internally. They're the same thing.

📖 Summary


📝 Exercises

  1. Basic (difficulty ⭐) Build a simple tab switcher:

    • 3 tabs (Home/Profile/Settings)
    • Switch dynamically with <component :is>
    • Log "Component X created/destroyed" on every switch
  2. Intermediate (difficulty ⭐⭐) Implement tabs + async loading:

    • 3 tabs, with SettingsTab loaded via defineAsyncComponent
    • Show a loading state (hand-written v-if="loading")
    • Switching back to HomeTab must not reload it
  3. Challenge (difficulty ⭐⭐⭐) Build a complete dynamic + async + caching system:

    1. 5 tabs, at least 2 loaded asynchronously
    2. Wrap them in <Suspense>
    3. Cache with <keep-alive :max="3">
    4. Log onActivated/onDeactivated on every switch
    5. Show an ErrorComponent when an async component fails
    6. Measure first-screen load time (before/after async)
    7. Strongly typed dynamic components in TypeScript
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏