Tabs, wizards, dashboards with pluggable widgets: many interfaces need to render one of several components chosen at runtime. Vue's <component :is> handles the switch, <KeepAlive> preserves the state of components you switch away from, and defineAsyncComponent loads rarely used components only when needed. After this lesson you will combine all three to build fast, stateful tab-style interfaces.
The built-in <component> element renders whatever :is evaluates to: a component object, or a string naming a registered component or native element:
<script setup>
import { ref } from 'vue'
import ProfileTab from './ProfileTab.vue'
import OrdersTab from './OrdersTab.vue'
const tabs = { profile: ProfileTab, orders: OrdersTab }
const current = ref('profile')
</script>
<template>
<nav>
<button v-for="(_, name) in tabs" :key="name" @click="current = name">
{{ name }}
</button>
</nav>
<component :is="tabs[current]" :user-id="42" @saved="onSaved" />
</template>Props and listeners on <component> are passed to whichever component is active. :is also accepts native tag names (<component :is="level"> with level = 'h2'), which is handy for a heading component whose level is a prop.
By default, switching unmounts the old component and mounts the new one, so any local state (form input, scroll position, fetched data) is lost.
Wrap the dynamic component in <KeepAlive> to cache inactive instances instead of destroying them:
<template>
<KeepAlive :include="['ProfileTab', 'OrdersTab']" :max="5">
<component :is="tabs[current]" />
</KeepAlive>
</template>include and exclude accept a comma-separated string, a RegExp or an array of component names. With <script setup>, the name is inferred from the file name, or set explicitly with defineOptions({ name: 'ProfileTab' }).max limits cached instances; the least recently used one is destroyed when the limit is exceeded.A cached component is not mounted again when it returns, so onMounted does not fire. Use the dedicated hooks instead:
import { onActivated, onDeactivated } from 'vue'
onActivated(() => startPolling()) // also runs on first mount
onDeactivated(() => stopPolling()) // also runs on unmount<KeepAlive> also works around <RouterView> to cache route components. Keep in mind that caching trades memory for speed; do not cache large lists you will rarely revisit.
Every statically imported component ends up in the initial bundle. defineAsyncComponent wraps a dynamic import() so the code is split into its own chunk and fetched the first time the component renders:
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorBox from './ErrorBox.vue'
const SettingsTab = defineAsyncComponent({
loader: () => import('./SettingsTab.vue'),
loadingComponent: LoadingSpinner,
delay: 200, // ms before showing the loading component
errorComponent: ErrorBox,
timeout: 10000 // ms after which the error component is shown
})The short form defineAsyncComponent(() => import('./SettingsTab.vue')) is enough in most cases. The result is a normal component: register it, pass props to it, use it in :is, or wrap it in <KeepAlive>. Vite emits a separate file for each dynamically imported module, which you can verify in the dist/assets folder after npm run build.
Good candidates for async loading are heavy, rarely visited pieces: rich-text editors, chart libraries, admin panels and modal dialogs. Route components are usually loaded lazily in the router configuration instead, which the Vue Router lesson covers.
v-if on multiple components when <component :is> would express the choice in one line.onMounted to run every time a kept-alive component reappears; use onActivated.include when the component's inferred name differs (for example index.vue); set defineOptions({ name }) explicitly.<KeepAlive> around several children; it requires exactly one child element.What does `<KeepAlive>` change about a dynamic component?
<component :is="..."> renders a component (or element) chosen at runtime; props and listeners pass through.<KeepAlive>.include, exclude and max control the cache; use onActivated/onDeactivated for cached components.defineAsyncComponent(() => import(...)) splits a component into its own chunk loaded on first use.delay and timeout.Next lesson: Teleport and Suspense — render content outside the component tree and coordinate async setup with a fallback.