Dynamic and async components
Switch components with :is, keep their state with KeepAlive, and download heavy ones only when needed with defineAsyncComponent().
What you'll learn
When you finish, you can do these.- 1Switch tabs on a page with <component :is>
- 2Keep a tab's state while it is hidden with KeepAlive
- 3Load a heavy chart on request with defineAsyncComponent()
- 4Fix a tab that shows nothing and a chart stuck loading
Picture a page with two tabs: Home holds a box for a note, and Stats counts your clicks. Only one tab shows at a time, and a note half typed in Home should survive a look at Stats. Under the tabs, a button opens a sales chart. In a real app, the chart pulls in a large charting library, and visitors who never click should not have to download it.
The Code tab holds that page: App.vue, with each tab and the chart as a component in a file of its own. By the end you can explain how it switches tabs, keeps the note and loads the chart on a click. You need one idea from Reactivity in depth: shallowRef(), a ref that tracks only when you replace its value, not what is inside it.
Switch components with
<component :is> shows one of several components in the same spot, which is how the page switches tabs. App.vue imports the two tabs from HomeTab.vue and StatsTab.vue. It lists them in a plain array, tabs, and keeps the one on show in current:
const tabs = [
{ name: 'Home', component: HomeTab },
{ name: 'Stats', component: StatsTab },
]
const current = shallowRef(HomeTab)
current holds the imported component itself, in a shallowRef, because Vue only needs to know when you swap it. Put in a ref() or reactive(), a component gets wrapped in a proxy, a stand-in object Vue uses to watch its insides, and Vue warns.
In the template, each button sets current, and <component> renders whatever current holds. A <component> tag with :is is called a dynamic component:
<nav>
<button v-for="tab in tabs" :key="tab.name" @click="current = tab.component">{{ tab.name }}</button>
</nav>
<component :is="current" />
Click Stats, and the counter takes the note box's place.
Give :is the object, not its name in quotes: What goes wrong shows why. A string suits a plain HTML tag: :is="'h2'" renders an <h2>.
On its own, <component :is> unmounts the tab you leave, which removes it and its state: a note typed in Home would be gone when you return.
Keep state with KeepAlive
<KeepAlive> keeps the tab you leave in memory, in a cache, so its state survives the switch. App.vue wraps the dynamic component in it:
<KeepAlive>
<component :is="current" />
</KeepAlive>
Type a note in Home, open Stats, open Home again: the note is still there.
A cached component is not unmounted, so its onUnmounted() clean-up does not run when you leave. Its onDeactivated() code runs instead, and its onActivated() code each time it is shown, the first time included. StatsTab.vue counts its visits that way, in a ref, shown, printed as Times shown:
onActivated(() => { shown.value++ })
Open Stats, visit Home, open Stats again, and the tab reads Times shown: 2.
Caching costs memory, so cache what is expensive to rebuild. The max prop on <KeepAlive> caps the cache: with :max="5", a sixth component pushes out the one used least recently.
Two more props, include and exclude, pick components by name, such as include="StatsTab"; a <script setup> component is named after its file.
Load later with defineAsyncComponent()
defineAsyncComponent() keeps a heavy component out of the JavaScript every visitor downloads, and fetches it when it is first needed. In the Code tab, HeavyChart.vue stands in for the sales chart and its large library.
A normal import line at the top of App.vue would put the chart in that download. defineAsyncComponent() takes a loader function instead, usually () => import('./HeavyChart.vue'). Unlike that line, the import() call loads the file only when the function runs, and Vue runs it the first time the chart renders.
When Vite, the build tool, builds the site, it turns that import() into a separate file, a chunk, which the browser fetches at that moment. A loading component can fill the wait, shown only after a short delay.
- Click Load
- import() fetches the chunk
- after delayloading component
- chunk arrives
- component renders
defineAsyncComponent() can also take an object: the loader, plus options for what the visitor sees while waiting. The Code tab has two small components for them: LoadingNote.vue says Loading the chart…, and ErrorNote.vue says what went wrong. In a real app, the object would read:
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingNote,
errorComponent: ErrorNote,
delay: 200,
timeout: 5000,
})
loadingComponent appears after delay milliseconds (200 is also the default), so a fast load does not flash it. errorComponent replaces it when the loader fails or takes longer than timeout, and receives the error as its error prop.
The Code tab's App.vue passes the same options with one change: its loader fakes a slow network. It returns a promise, JavaScript's value for a result that comes later, and setTimeout hands over the chart after 1.2 seconds:
loader: () => new Promise<typeof import('./HeavyChart.vue')>((resolve, reject) => {
setTimeout(() => resolve(import('./HeavyChart.vue')), 1200)
}),
Click Load the chart: Loading the chart… appears, then the bars of Sales this week. The unused reject is for Try it 2.
What goes wrong
The first mistake gives :is the tab's name in quotes instead of the component.
<component :is="'HomeTab'" />
Nothing appears and nothing warns. A string finds only components registered by name, as app.component('HomeTab', HomeTab) does for the whole app, or a components option. An import in <script setup> is neither, so Vue rendered an empty <hometab> element, the way it renders a tag it does not know. Pass the imported component:
<component :is="HomeTab" />
The Home tab's text and note box appear again.
The second mistake has no plan for a failed download.
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingNote,
})
On a flaky connection the loader fails. The page keeps saying Loading the chart… for good, and the failure shows only as an error in the console. Give the visitor an error state, and a timeout so a stalled download ends too:
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingNote,
errorComponent: ErrorNote,
timeout: 5000,
})
A failed or stalled load now shows ErrorNote: Could not load the chart, then the error's message.
What to remember
<component :is>renders the component object you give it. A string only finds components registered by name, or plain HTML tags.- Keep components in plain objects, arrays or a
shallowRef, never inref()orreactive(). <KeepAlive>caches components you leave. They getonActivated()andonDeactivated(), andinclude,excludeandmaxlimit the cache.defineAsyncComponent()fetches a component's code on first render. SetloadingComponentwithdelay, and always anerrorComponentwithtimeout.- Inside
<Suspense>, the fallback replacesloadingComponent,delayandtimeoutunlesssuspensible: false.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Open Stats, click its button three times, switch to Home and back. Clicked 3 times and Times shown: 2 tell you KeepAlive kept the tab. Now add
:max="1"to<KeepAlive>and do the same: Stats comes back with Clicked 0 times and Times shown: 1, because the cache holds one component and Home pushed Stats out. - Make the download fail. In the loader, replace
resolve(import('./HeavyChart.vue'))withreject(new Error('Network down'))and click Load the chart. After the loading note, ErrorNote shows Could not load the chart: Network down, and the preview reports the error too, as Vue does in development. Now delete theerrorComponentline and try again: Loading the chart… stays on screen for good. - Change
shallowRef(HomeTab)toref(HomeTab). The tabs still switch, but the preview warnsVue received a Component that was made a reactive object. This can lead to unnecessary performance overhead and should be avoided by marking the component with `markRaw` or using `shallowRef` instead of `ref`.Change it back toshallowRef, and the warning does not return.