Updated June 2026. Tested on Vue 3 with
<script setup>. Part of the Techalyst Vue 3 series.
Tabs, wizards, and switchable panels all need to render a different component depending on state. Vue's built-in <component> tag does exactly that, and <KeepAlive> solves the state-loss problem that comes with it. Here is how both work together.
The component tag
<component> is a placeholder whose :is prop decides which component renders. Bind it to state and changing that state swaps the component.
<script setup>
import { shallowRef } from 'vue'
import Profile from './Profile.vue'
import Settings from './Settings.vue'
const tabs = { Profile, Settings }
const current = shallowRef(Profile)
</script>
<template>
<button @click="current = Profile">Profile</button>
<button @click="current = Settings">Settings</button>
<component :is="current" />
</template>
In <script setup> you can pass :is either a registered component name or the imported component object directly. Using the object is the cleaner pattern, and shallowRef is the right container for it, you do not want Vue making the whole component definition deeply reactive. The <component> tag also accepts props, attributes, events, and refs just like any normal tag.
Every switch is a fresh start
Without help, swapping :is fully destroys the old component and creates a new one. Any local state in the component you left, a half-filled form, a scroll position, is gone when you come back.
load Profile -> Profile mounted
switch -> Profile unmounted, Settings mounted
switch back -> Settings unmounted, Profile mounted (brand new, state reset)
For a lot of UI that is fine. For tabs where users flip back and forth, losing their input is not.
KeepAlive preserves instances
Wrap the dynamic component in <KeepAlive> and Vue caches the instance instead of destroying it. State survives the round trip.
<template>
<KeepAlive>
<component :is="current" />
</KeepAlive>
</template>
Now a form half-filled in the Profile tab is still half-filled when you switch away and back. One consequence: because the component is never unmounted while cached, onUnmounted does not fire on a switch. Vue gives you two dedicated lifecycle hooks for the cached case instead.
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => { /* came back into view */ })
onDeactivated(() => { /* switched away, still cached */ })
</script>
onActivated fires each time the component is shown, onDeactivated each time it is hidden. Note neither fires during server-side rendering.
Controlling the cache
Caching everything forever costs memory, so <KeepAlive> takes three props to scope it.
maxcaps how many instances stay cached, evicting the least recently used when full.includecaches only components whosenamematches.excludecaches everything except the matched names.
<template>
<KeepAlive :max="5">
<component :is="current" />
</KeepAlive>
<KeepAlive include="Profile,Settings">
<component :is="current" />
</KeepAlive>
</template>
include and exclude match on the component's name, so in <script setup> give each component an explicit name (via a separate <script> block or defineOptions) if you want to target it.
<script setup>
defineOptions({ name: 'Profile' })
</script>
Forcing a remount with key
Sometimes you want the opposite of caching: a guaranteed fresh instance. Changing the :key on an element tells Vue to throw the old one away and build a new one, even when :is has not changed.
<template>
<component :is="current" :key="recordId" />
</template>
This is the idiomatic fix for the classic "route changed but the component reused stale data" problem, key the component by the id it depends on and switching ids gives you a clean mount. One caution: when combining :key with <KeepAlive>, make the key unique per component (for example include the component name) so cached instances do not collide.
Wrapping up
Reach for <component :is> whenever the rendered component depends on state, holding the target in a shallowRef. Remember each plain switch resets state, and wrap with <KeepAlive> when that matters, using onActivated/onDeactivated for the cached lifecycle and max/include/exclude to keep memory in check. When you instead need a deliberate fresh start, change the :key.
More in the series: teleport in Vue 3 and Vue 3 lifecycle hooks. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.