Chapter 49: Vue KeepAlive Component
The <KeepAlive> component
This is one of the built-in special components that Vue gives you for free — no installation, no import — and once you understand it, you’ll wonder how you ever built tabbed interfaces, multi-step wizards, or dashboards without it.
What does <KeepAlive> actually do? (Very simple human explanation)
Normally when a component is hidden (via v-if, router navigation, dynamic :is, etc.), Vue destroys it completely:
- All internal state is lost
- All DOM is removed
- All event listeners are cleaned up
- Next time it appears → it mounts from scratch (re-runs setup, fetches data again, resets forms, scrolls to top, etc.)
<KeepAlive> changes this behavior:
It keeps the component alive in memory even when it’s not visible on the screen.
So when the component is shown again:
- It does NOT re-mount / re-run setup
- It keeps its internal state (form values, checked checkboxes, scroll position, fetched data…)
- It keeps its DOM tree (just hidden)
- Vue only activates/deactivates it (runs special activated / deactivated hooks)
This creates the feeling of a native multi-page app even though it’s still a single-page application (SPA).
Most Common & Most Valuable Use-Cases (2026)
| Use-case | Without <KeepAlive> | With <KeepAlive> | Why users love it |
|---|---|---|---|
| Tabs (dashboard, settings, profile) | Switch tab → form resets, data refetched | Switch tab → form stays filled, scroll position saved | Huge UX win |
| Router-view with many routes | Navigate back → list resets to top, filters lost | Navigate back → exact same scroll position & filters | Feels native |
| Multi-step wizard / checkout flow | Go back → previous step resets | Go back → all entered data preserved | Prevents rage-quit |
| Chat / messaging app | Switch conversation → message list resets | Switch back → exact scroll position & unread state saved | Feels like real chat |
| Data-heavy tabs (charts, tables) | Switch tab → chart re-renders, API called again | Switch back → chart stays exactly as left (zoomed, etc.) | Saves time & API calls |
Real, Practical Example – Tabbed Dashboard with <KeepAlive>
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<!-- DashboardView.vue --> <template> <div class="dashboard"> <h1>My Dashboard</h1> <div class="tabs"> <button v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }" @click="activeTab = tab.name" > {{ tab.label }} </button> </div> <!-- The magic wrapper --> <KeepAlive> <component :is="currentTabComponent" :key="activeTab" <!-- optional but good practice --> /> </KeepAlive> </div> </template> <script setup lang="ts"> import { ref, computed, defineAsyncComponent } from 'vue' const Overview = () => import('@/components/tabs/Overview.vue') const Analytics = defineAsyncComponent(() => import('@/components/tabs/Analytics.vue')) const Settings = () => import('@/components/tabs/Settings.vue') const tabs = [ { name: 'overview', label: 'Overview', component: Overview }, { name: 'analytics', label: 'Analytics', component: Analytics }, { name: 'settings', label: 'Settings', component: Settings } ] const activeTab = ref('overview') const currentTabComponent = computed(() => { return tabs.find(t => t.name === activeTab.value)?.component || null }) </script> <style scoped> .tabs { display: flex; gap: 1rem; margin-bottom: 2rem; } .tabs button { padding: 0.8rem 1.5rem; border: 1px solid #ccc; border-radius: 6px; background: white; cursor: pointer; } .tabs .active { background: #3b82f6; color: white; border-color: #3b82f6; } </style> |
Now let’s create one of the tab components that has state (a form) — so you can see the difference.
Analytics.vue (has form + scrollable content)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<template> <div class="analytics-tab"> <h2>Analytics Settings</h2> <!-- Form that should NOT reset when switching tabs --> <div class="form-group"> <label>Date Range</label> <input type="date" v-model="dateRange.start" /> <input type="date" v-model="dateRange.end" /> </div> <div class="form-group"> <label>Metric</label> <select v-model="selectedMetric"> <option value="revenue">Revenue</option> <option value="users">New Users</option> <option value="orders">Orders</option> </select> </div> <!-- Long scrollable content to test scroll position --> <div class="long-content"> <p v-for="n in 50" :key="n">Line {{ n }} — scroll position should be preserved</p> </div> </div> </template> <script setup lang="ts"> import { reactive } from 'vue' const dateRange = reactive({ start: '2026-01-01', end: '2026-02-20' }) const selectedMetric = ref('revenue') </script> <style scoped> .long-content { height: 600px; overflow-y: auto; border: 1px solid #eee; padding: 1rem; margin-top: 1rem; } </style> |
What happens without <KeepAlive>
- Switch to Overview → Analytics unmounts → form resets to defaults, scroll goes to top
- Switch back → Analytics re-mounts → API would be called again, form empty, scroll at top
With <KeepAlive>
- Switch away → Analytics cached in memory
- Switch back → exact same state: form values preserved, scroll position preserved, no re-fetch
Extra Features & Control (2026 Details)
| Feature | Syntax / Prop | What it does / When to use |
|---|---|---|
| Cache only specific components | <KeepAlive include=”Overview,Analytics”> | Only cache named components |
| Exclude some | <KeepAlive exclude=”HeavyChart”> | Prevent caching heavy ones |
| Max cached instances | <KeepAlive max=”10″> | Limit memory usage |
| Check if cached | onActivated(() => console.log(‘Tab activated’)) | Run code when tab shown again |
| Check when deactivated | onDeactivated(() => console.log(‘Tab hidden’)) | Pause timers, save draft, etc. |
Summary Table – When to Use <KeepAlive>
| Situation | Use <KeepAlive>? | Why / Alternative |
|---|---|---|
| Tabs with forms, filters, scroll | Yes | Preserves UX & state |
| Wizard / checkout steps | Yes | Don’t lose entered data |
| Router-view with many pages | Yes (around <router-view>) | Back/forward feels native |
| Very heavy component (charts, maps) | Usually no | Better to re-mount and re-fetch |
| Modal / dialog | No | Should always start fresh |
| List with pagination / infinite scroll | Usually no | State belongs to parent |
Pro Tips from Real Projects (Hyderabad 2026)
- Always test mobile — scroll position preservation feels amazing on touch devices
- Combine with <Transition> for smooth tab switch animations
- Use onActivated / onDeactivated instead of onMounted/onUnmounted when inside <KeepAlive>
- For very large apps → limit max prop to avoid memory issues
- Don’t wrap everything in <KeepAlive> — it keeps components in memory forever
Your mini practice task:
- Build a simple 3-tab dashboard (Overview, Settings, Profile)
- Put <KeepAlive> around the dynamic <component :is>
- Add a form + long scrollable content in Settings tab
- Switch tabs → go back → see that form values & scroll position are preserved
Any part confusing? Want full examples for:
- <KeepAlive> + <router-view> + transition?
- onActivated / onDeactivated with timers & API polling?
- <KeepAlive> + <Transition> for animated tab switch?
- Real dashboard with lazy-loaded tabs?
Just tell me — we’ll build the next smooth, cached tab system together 🚀
