Dynamic Components, KeepAlive and Async Components

Intermediate
12 min

Dynamic Components, KeepAlive and Async Components

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.

Switching Components with component :is

The built-in <component> element renders whatever :is evaluates to: a component object, or a string naming a registered component or native element:

vue
<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.

Preserving State with KeepAlive

Wrap the dynamic component in <KeepAlive> to cache inactive instances instead of destroying them:

vue
<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:

javascript
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.

Loading Components on Demand

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:

javascript
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.

Common mistakes

  • Using v-if on multiple components when <component :is> would express the choice in one line.
  • Expecting onMounted to run every time a kept-alive component reappears; use onActivated.
  • Passing a file-name string to include when the component's inferred name differs (for example index.vue); set defineOptions({ name }) explicitly.
  • Wrapping <KeepAlive> around several children; it requires exactly one child element.
Quick Quiz
Question 1 of 2

What does `<KeepAlive>` change about a dynamic component?

Key Takeaways

  • <component :is="..."> renders a component (or element) chosen at runtime; props and listeners pass through.
  • Switching unmounts the previous component unless it is wrapped in <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.
  • The full options form adds loading and error components, delay and timeout.

Next lesson: Teleport and Suspense — render content outside the component tree and coordinate async setup with a fallback.

Dynamic Components, KeepAlive and Async Components - Vue.js | CodeYourCraft | CodeYourCraft