| |

Vue.js 47 🟢 Preserving Component State with (include, exclude, max)

⏱️ Estimated read: 16 min | |

Swapping one component for another destroys the first and creates the second. The destroyed component loses everything: the form the user was halfway through, the scroll position in a long list, the cursor in a text field, the fetched data that took a second to load. In a tabbed interface or a wizard, this is rarely what the user wants. The <KeepAlive> component is the built-in that changes the rule. It caches the inactive component instances and restores them when they become active again, so switching tabs does not reset the state.

Key point: <KeepAlive> caches component instances, not their DOM. The cached component is deactivated rather than unmounted, and its DOM is removed from the document. When the component is activated, its DOM is reinserted, and the state is exactly as it was. The cache has a default limit of 12 components unless :max is set.


Why KeepAlive exists

The state-loss problem. A form with ten fields, half-filled, is lost when the user clicks a different tab. They must start over. A long list loses its scroll position, forcing the user to scroll back. A text editor loses the cursor and the undo history. None of this is necessary if the component’s state is preserved.

The fetch problem. A component that fetches data on mount runs the fetch every time it is created. For a tab that shows a dashboard, switching back and forth triggers the same API call repeatedly. The cached component avoids the refetch because it was never unmounted.

The lifecycle problem. A component’s onMounted and onUnmounted hooks fire on every swap. Side effects that should run once run many times: an analytics event, a WebSocket subscription, a timer. The <KeepAlive> replaces these hooks with onActivated and onDeactivated, which fire only when the component becomes visible or hidden.

The performance problem. Creating a component is more expensive than reinserting a cached one. For a heavy component with a complex template, the difference is measurable. The cache trades memory for speed.

The memory problem. A cache that grows without bound is a leak. Every cached component holds its state, its DOM, and its references. The :max prop bounds the cache, and the least recently used component is evicted when the limit is reached.

The routing problem. A multi-page application keeps the previous page’s state when the user returns. A single-page application loses it. The <KeepAlive> with the router preserves the state across route changes, which is what users expect from a page.

The scroll problem. The scroll position of a long list is part of the user’s context. Losing it forces a re-scroll. The <KeepAlive> preserves it, and onActivated can restore it explicitly if the browser has reset it.


a. The basic usage

The <KeepAlive> wraps a dynamic component. The wrapped component is cached when it is swapped out and restored when it is swapped back in.

The simplest form

<template>
  <KeepAlive>
    <component :is="currentComponent" />
  </KeepAlive>
</template>

The <KeepAlive> has no props. It caches every component that is rendered inside it, up to the default limit of 12 instances.

The lifecycle change

Inside a <KeepAlive>, the onMounted hook fires once when the component is created, and the onActivated hook fires every time the component becomes visible. The onUnmounted hook does not fire until the cache evicts the component or the parent is unmounted.

<script setup>
import { onMounted, onActivated, onDeactivated, onUnmounted } from 'vue';

onMounted(() => {
  console.log('mounted');         // Fires once
});

onActivated(() => {
  console.log('activated');       // Fires on every show
});

onDeactivated(() => {
  console.log('deactivated');     // Fires on every hide
});

onUnmounted(() => {
  console.log('unmounted');       // Fires on eviction
});
</script>

The three hooks have distinct roles: onMounted for one-time setup, onActivated for refresh, and onDeactivated for pausing.

The template ref

A ref on a cached component resolves only when the component is active. When the component is deactivated, the ref is null.

<KeepAlive>
  <component :is="currentComponent" ref="childRef" />
</KeepAlive>

The childRef.value is the component instance when it is active and null when it is cached.

The <Transition> composition

The <KeepAlive> composes with <Transition>. The transition wraps the cached component and animates the enter and leave.

<Transition name="fade" mode="out-in">
  <KeepAlive>
    <component :is="currentComponent" :key="currentComponent.name" />
  </KeepAlive>
</Transition>

The :key is required for the transition to fire when the component changes.

The <Suspense> composition

The <KeepAlive> composes with <Suspense>. An async component inside the cache is cached after its first resolution.

<Suspense>
  <KeepAlive>
    <component :is="asyncComponent" />
  </KeepAlive>
</Suspense>

The nested <KeepAlive>

A <KeepAlive> cannot be nested inside another <KeepAlive> for the same component. The outer cache takes precedence, and the inner cache is ignored. The <KeepAlive> should be placed at the top of the tree.


b. The include and exclude props

The <include> and <exclude> props filter which components are cached. The values are comma-separated strings, arrays of strings, or regular expressions, and they match against the component’s name.

The include

The <include> prop lists the components to cache. A component not in the list is not cached.

<KeepAlive include="TabHome,TabProfile">
  <component :is="currentComponent" />
</KeepAlive>

Only TabHome and TabProfile are cached. The other components are created and destroyed on each swap.

The array form

The prop accepts an array of strings.

<KeepAlive :include="['TabHome', 'TabProfile']">
  <component :is="currentComponent" />
</KeepAlive>

The regex form

The prop accepts a regular expression.

<KeepAlive :include="/^Tab/">
  <component :is="currentComponent" />
</KeepAlive>

The pattern matches every component whose name starts with Tab.

The exclude

The <exclude> prop lists the components not to cache. It is the inverse of <include>.

<KeepAlive exclude="TabSettings">
  <component :is="currentComponent" />
</KeepAlive>

Every component is cached except TabSettings.

The include and exclude together

When both are present, <exclude> takes precedence.

<KeepAlive include="/^Tab/" exclude="TabSettings">
  <component :is="currentComponent" />
</KeepAlive>

The TabSettings component matches both, and the exclude wins. It is not cached.

The component name requirement

The include and exclude match against the component’s name. A component defined with <script setup> uses its filename as the name. A component that needs a specific name can set it with a separate <script> block or defineOptions.

<script>
export default {
  name: 'TabHome',
};
</script>

<script setup>
// ...
</script>

The name is required for the include and exclude to work. A component without a name is never matched and is cached by default when no filter is present.

The dynamic include

The include and exclude props are reactive. Changing them changes the filter on the next render.

<script setup>
import { ref } from 'vue';
const cacheTabs = ref(['TabHome', 'TabProfile']);
</script>

<template>
  <KeepAlive :include="cacheTabs">
    <component :is="currentComponent" />
  </KeepAlive>
</template>

The filter is recomputed when cacheTabs changes.

The filter’s effect on existing caches

When a component is removed from the include list, its cached instance is not immediately evicted. The cache remains until the component is unmounted or the cache is otherwise cleared. The Vue documentation notes that the cache is not pruned reactively.


c. The max prop

The <max> prop limits the number of cached instances. When the limit is exceeded, the least recently used instance is evicted.

The basic usage

<KeepAlive :max="5">
  <component :is="currentComponent" />
</KeepAlive>

The cache holds at most five components. The sixth swap evicts the least recently used one.

The LRU eviction

The eviction policy is least recently used. The component that has been inactive the longest is removed when the cache is full.

Cache capacity: 3

Access order: A, B, C
Cache: [A, B, C]

Access D:
Cache: [B, C, D]  (A was evicted)

Access B:
Cache: [C, D, B]  (B is now the most recent)

The LRU order is maintained by the <KeepAlive> internally.

The eviction lifecycle

An evicted component is unmounted. Its onUnmounted hook fires, and its state is lost.

<script setup>
import { onUnmounted } from 'vue';

onUnmounted(() => {
  console.log('evicted');  // Fires when the cache evicts the component
});
</script>

The default limit

The default max is 12. A <KeepAlive> without a max prop holds up to 12 instances before evicting.

The combination with include

The :max and :include compose. The cache holds at most :max components, and only those matching :include are eligible.

<KeepAlive :include="['TabHome', 'TabProfile', 'TabReports']" :max="2">
  <component :is="currentComponent" />
</KeepAlive>

Only three components match the include, and the cache holds at most two of them.

The unbounded cache

A <KeepAlive> without an include, exclude, or max caches every component it renders, up to the default of 12. For a dynamic list of components that grows without bound, the explicit :max is required.


Complete Example Session

<!-- ============================================
     PART 1: THE BASIC KEEPALIVE
     ============================================ -->

<script setup>
import { shallowRef } from 'vue';
import TabHome from './TabHome.vue';
import TabSettings from './TabSettings.vue';

const current = shallowRef(TabHome);
</script>

<template>
  <KeepAlive>
    <component :is="current" />
  </KeepAlive>
  <button @click="current = TabHome">Home</button>
  <button @click="current = TabSettings">Settings</button>
</template>

<!-- ============================================
     PART 2: THE LIFECYCLE HOOKS
     ============================================ -->

<script setup>
import { onMounted, onActivated, onDeactivated, onUnmounted } from 'vue';

onMounted(() => console.log('mounted'));
onActivated(() => console.log('activated'));
onDeactivated(() => console.log('deactivated'));
onUnmounted(() => console.log('unmounted'));
</script>

<!-- ============================================
     PART 3: THE INCLUDE FILTER
     ============================================ -->

<KeepAlive include="TabHome,TabProfile">
  <component :is="current" />
</KeepAlive>

<!-- ============================================
     PART 4: THE REGEX FILTER
     ============================================ -->

<KeepAlive :include="/^Tab/">
  <component :is="current" />
</KeepAlive>

<!-- ============================================
     PART 5: THE EXCLUDE FILTER
     ============================================ -->

<KeepAlive exclude="TabSettings">
  <component :is="current" />
</KeepAlive>

<!-- ============================================
     PART 6: THE MAX LIMIT
     ============================================ -->

<KeepAlive :max="5">
  <component :is="current" />
</KeepAlive>

<!-- ============================================
     PART 7: THE DYNAMIC FILTER
     ============================================ -->

<script setup>
import { ref, shallowRef } from 'vue';
const cacheTabs = ref(['TabHome', 'TabProfile']);
const current = shallowRef(TabHome);
</script>

<template>
  <KeepAlive :include="cacheTabs">
    <component :is="current" />
  </KeepAlive>
</template>

<!-- ============================================
     PART 8: THE TRANSITION COMPOSITION
     ============================================ -->

<Transition name="fade" mode="out-in">
  <KeepAlive>
    <component :is="current" :key="current.name" />
  </KeepAlive>
</Transition>

<!-- ============================================
     PART 9: THE NAMED COMPONENT
     ============================================ -->

<!-- TabHome.vue -->
<script>
export default { name: 'TabHome' };
</script>

<script setup>
// ...
</script>

<!-- ============================================
     PART 10: THE COMPLETE PATTERN
     ============================================ -->

<!--
1. Wrap the dynamic component with <KeepAlive>.
2. Use onActivated and onDeactivated instead of onMounted and onUnmounted.
3. Use :include to list the components to cache.
4. Use :exclude to list the components not to cache.
5. Use :max to limit the cache size.
6. Name the components so the filters work.
7. Compose with <Transition> and <Suspense>.
8. The cache preserves the state and the scroll position.
-->

The ten parts showed the basic <KeepAlive>, the lifecycle hooks, the include filter, the regex filter, the exclude filter, the max limit, the dynamic filter, the transition composition, the named component, and the complete pattern.


Quick Reference

The KeepAlive Props

PropTypePurpose
includeString, Array, RegExpThe components to cache
excludeString, Array, RegExpThe components not to cache
maxNumberThe maximum cache size

The Lifecycle Hooks

HookWhen
onMountedOnce, on creation
onActivatedEvery time the component becomes visible
onDeactivatedEvery time the component is hidden
onUnmountedOn eviction or parent unmount

The Default Behavior

AspectDefault
Cache size12
Eviction policyLeast recently used
Filter matchComponent name

The Name Sources

SetupName
<script setup>Filename
Options APIname property
defineOptionsname option

The Filter Forms

FormExample
Stringinclude="TabHome,TabProfile"
Array:include="['TabHome', 'TabProfile']"
RegExp:include="/^Tab/"

Best Practices

✅ Do This:

<!-- Use onActivated for refresh logic. -->
onActivated(() => {
  refreshData();
});  <!-- ✅ -->

<!-- Use onDeactivated for pause logic. -->
onDeactivated(() => {
  clearInterval(timer);
});  <!-- ✅ -->

<!-- Use :include to cache only specific components. -->
<KeepAlive include="TabHome,TabProfile">  <!-- ✅ -->

<!-- Use :max to bound the cache. -->
<KeepAlive :max="5">  <!-- ✅ -->

<!-- Name the components for the filters. -->
<script>export default { name: 'TabHome' };</script>  <!-- ✅ -->

<!-- Use :key with the transition. -->
<Transition mode="out-in">
  <KeepAlive><component :is="current" :key="current.name" /></KeepAlive>
</Transition>  <!-- ✅ -->

❌ Don’t Do This:

<!-- Don't rely on onMounted for refresh logic. -->
onMounted(() => {
  refreshData();  <!-- ❌ Fires only once -->
});  <!-- ❌ -->

<!-- Don't forget the include when the list is dynamic. -->
<KeepAlive>  <!-- ❌ Unbounded cache -->
  <component :is="current" />
</KeepAlive>  <!-- ❌ -->

<!-- Don't nest KeepAlive for the same component. -->
<KeepAlive><KeepAlive><component :is="current" /></KeepAlive></KeepAlive>  <!-- ❌ -->

<!-- Don't expect the ref to work when cached. -->
childRef.value.method();  <!-- ❌ null when deactivated -->

<!-- Don't forget to name the components. -->
<KeepAlive include="TabHome">  <!-- ❌ The component has no name -->

Common Pitfalls

PitfallWhy It HappensFix
State not preservedNo <KeepAlive>Wrap the component
Refresh not runningUsing onMountedUse onActivated
Filter not matchingComponent has no nameName the component
Cache grows unboundedNo :maxSet the limit
Transition does not fireMissing :keyAdd the key
Ref is nullComponent is deactivatedCheck before use
Filter not reactiveStatic includeUse a ref or computed

Real-World Examples

1. Basic KeepAlive

<KeepAlive><component :is="current" /></KeepAlive>

2. Lifecycle Hooks

onActivated(() => refreshData());
onDeactivated(() => pauseTimer());

3. Include Filter

<KeepAlive include="TabHome,TabProfile">

4. Exclude Filter

<KeepAlive exclude="TabSettings">

5. Max Limit

<KeepAlive :max="5">

6. Dynamic Filter

<KeepAlive :include="cacheTabs">

7. Transition Composition

<Transition mode="out-in">
  <KeepAlive><component :is="current" :key="current.name" /></KeepAlive>
</Transition>

8. Named Component

<script>export default { name: 'TabHome' };</script>

9. The Regex Filter

<KeepAlive :include="/^Tab/">

10. The Complete Pattern

<Transition mode="out-in">
  <KeepAlive :include="cacheTabs" :max="5">
    <component :is="current" :key="current.name" />
  </KeepAlive>
</Transition>

Visual

The Cache Lifecycle

┌────────────────────────────────────────────────────────────┐
│  THE CACHE LIFECYCLE                                       │
│                                                            │
│  WITHOUT KEEPALIVE                                         │
│  ┌──────────────────────────────────────────────────┐     │
│  │  TabHome → TabSettings                            │     │
│  │    onUnmounted(TabHome)                           │     │
│  │    onMounted(TabSettings)                         │     │
│  │    TabHome's state is lost.                       │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  WITH KEEPALIVE                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  TabHome → TabSettings                            │     │
│  │    onDeactivated(TabHome)                         │     │
│  │    onMounted(TabSettings)                         │     │
│  │    onActivated(TabSettings)                       │     │
│  │    TabHome's state is preserved.                  │     │
│  │                                                   │     │
│  │  TabSettings → TabHome                            │     │
│  │    onDeactivated(TabSettings)                     │     │
│  │    onActivated(TabHome)                           │     │
│  │    TabHome's state is restored.                   │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Lifecycle Hooks

┌────────────────────────────────────────────────────────────┐
│  THE LIFECYCLE HOOKS                                       │
│                                                            │
│  OUTSIDE KEEPALIVE                                         │
│  ┌──────────────────────────────────────────────────┐     │
│  │  onMounted    → once, on creation                 │     │
│  │  onUnmounted  → once, on destruction              │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  INSIDE KEEPALIVE                                          │
│  ┌──────────────────────────────────────────────────┐     │
│  │  onMounted    → once, on creation                 │     │
│  │  onActivated  → every time the component is shown │     │
│  │  onDeactivated→ every time the component is hidden│     │
│  │  onUnmounted  → once, on eviction                 │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Use onActivated for refresh, onDeactivated for pause.     │
└────────────────────────────────────────────────────────────┘

The LRU Eviction

┌────────────────────────────────────────────────────────────┐
│  THE LRU EVICTION                                          │
│                                                            │
│  Cache capacity: 3                                         │
│                                                            │
│  Show A → Cache: [A]                                       │
│  Show B → Cache: [A, B]                                    │
│  Show C → Cache: [A, B, C]                                 │
│  Show A → Cache: [B, C, A]  (A is now most recent)        │
│  Show D → Cache: [C, A, D]  (B was least recent, evicted) │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The least recently used component is evicted     │     │
│  │  when the cache is full.                          │     │
│  │  The evicted component is unmounted.              │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Filter Behavior

┌────────────────────────────────────────────────────────────┐
│  THE FILTER BEHAVIOR                                       │
│                                                            │
│  include="TabHome,TabProfile"                              │
│  ┌──────────────────────────────────────────────────┐     │
│  │  TabHome    → cached                              │     │
│  │  TabProfile → cached                              │     │
│  │  TabSettings→ NOT cached (created each time)      │     │
│  │  TabReports → NOT cached                          │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  exclude="TabSettings"                                     │
│  ┌──────────────────────────────────────────────────┐     │
│  │  TabHome    → cached                              │     │
│  │  TabProfile → cached                              │     │
│  │  TabSettings→ NOT cached                          │     │
│  │  TabReports → cached                              │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  include="/^Tab/" exclude="TabSettings"                    │
│  ┌──────────────────────────────────────────────────┐     │
│  │  TabHome    → cached                              │     │
│  │  TabProfile → cached                              │     │
│  │  TabSettings→ NOT cached (exclude wins)           │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
<KeepAlive>Caches inactive component instances
includeThe components to cache
excludeThe components not to cache
maxThe maximum cache size
onActivatedFires when the component becomes visible
onDeactivatedFires when the component is hidden
Default cache size12
Eviction policyLeast recently used
Filter matchComponent name

Key takeaways:

  • <KeepAlive> caches the inactive components instead of destroying them. The cached component is deactivated, and its state is preserved. When the component becomes active again, it is restored with the state exactly as it was.
  • The lifecycle hooks change inside a <KeepAlive>. The onMounted hook fires once, and the onActivated hook fires every time the component becomes visible. The onDeactivated hook fires every time it is hidden, and the onUnmounted hook fires on eviction.
  • The include and exclude props filter the cache. The filters match against the component’s name, which comes from the filename in <script setup> or the name property in the Options API. The exclude takes precedence when both match.
  • The max prop bounds the cache. The default is 12. The least recently used component is evicted when the limit is reached, and the evicted component is unmounted.
  • The :key is required with <Transition>. Without the key, Vue reuses the DOM element, and the transition does not fire. The key forces a new element when the component changes.
  • The refs are null when the component is cached. A template ref on a cached component resolves only when the component is active. The ref is null when the component is deactivated.
  • The onActivated hook is for refresh logic. The onMounted hook fires only once, so data that changes while the component is cached must be refreshed in the onActivated hook.
  • The <KeepAlive> cannot be nested. A <KeepAlive> inside another <KeepAlive> for the same component is ignored, and the outer cache takes precedence.

Remember: The <KeepAlive> component preserves the state of the components it caches. The inactive components are deactivated instead of unmounted, and their DOM is removed from the document. When the component becomes active again, the DOM is reinserted and the state is exactly as it was. The include and exclude props filter which components are cached, and the max prop bounds the cache size. The onActivated and onDeactivated hooks replace the onMounted and onUnmounted hooks for the refresh and pause logic. The result is a tabbed interface, a wizard, or a router with the state preserved across the swaps, and a user experience that matches the expectation of a page that does not reset.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!