Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 081 / 100

KeepAlive in Vue: Vue & Nuxt best practices explained

Cache component state between switches.

Expona Admin8 min read

Introduction

If you've ever built a form-heavy feature in Vue where users switch between tabs or pages and lose their input, you know the frustration. KeepAlive in Vue: Vue & Nuxt best practices explained covers one of the most practical performance and UX tools available to us—component caching that preserves state across navigation without reinitializing the entire component tree. Whether you're managing a multi-step wizard, a dashboard with filtered views, or a SPA with expensive data fetches, understanding KeepAlive can transform how your application feels.

What is KeepAlive and Why It Matters

KeepAlive is a built-in Vue component that wraps dynamic components or router views and caches them in memory rather than destroying and recreating them on each switch. Without it, every navigation triggers a full component lifecycle reset—setup() re-runs, API calls re-fire, form inputs reset. In production, this compounds performance issues and creates a choppy user experience.

We use KeepAlive when:

  • Forms or input fields should retain user data during navigation
  • Expensive computations or API calls should be cached
  • Scroll positions should be preserved
  • We want to avoid component re-initialization overhead

The tradeoff is memory usage—cached components stay in the DOM, so we must be intentional about what we cache and set reasonable limits.

Wrapping Dynamic Components with KeepAlive

The simplest use case is wrapping a dynamic component. In a tabbed interface or modal switcher, you want inactive tabs to remain cached.

typescript
<template>
  <div class="tabs">
    <button
      v-for="tab in tabs"
      :key="tab"
      @click="activeTab = tab"
      :class="{ active: activeTab === tab }"
    >
      {{ tab }}
    </button>
  </div>

  <KeepAlive>
    <component :is="getComponent(activeTab)" :key="activeTab" />
  </KeepAlive>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'

const activeTab = ref('A')
const tabs = ['A', 'B', 'C']

const getComponent = (tab: string) => {
  const map: Record<string, any> = {
    A: TabA,
    B: TabB,
    C: TabC,
  }
  return map[tab]
}
</script>

Now when a user clicks between tabs, the component instance is reused. The setup() hook doesn't re-run; reactive state remains intact. Form inputs, API results, scroll position—all preserved. On real projects handling analytics dashboards or configuration panels, this cuts perceived latency dramatically.

Filtering with Include and Exclude

Not every component should be cached. Caching a login form or a one-time payment flow doesn't make sense. Use include and exclude to be surgical about what stays in memory.

Components are matched by their name option. Ensure your components have explicit names:

typescript
// TabA.vue
<script setup lang="ts">
defineOptions({ name: 'TabA' })
// component logic
</script>

Then filter in KeepAlive:

typescript
<template>
  <!-- Cache only TabA and TabB -->
  <KeepAlive include="TabA,TabB">
    <component :is="getComponent(activeTab)" :key="activeTab" />
  </KeepAlive>

  <!-- Or use regex -->
  <KeepAlive :include="/^Tab/">
    <component :is="getComponent(activeTab)" :key="activeTab" />
  </KeepAlive>

  <!-- Exclude specific components -->
  <KeepAlive :exclude="/^Modal/">
    <component :is="getComponent(activeTab)" :key="activeTab" />
  </KeepAlive>
</template>

We've seen teams cache everything by default and wonder why memory climbs. Being explicit with include or exclude is how you stay in control. In a CRM dashboard with 15 different views, you might cache only the heavy compute panels—filtered lists, charts, reports—and let lightweight detail views destroy and recreate freely.

Using KeepAlive with Router Views in Nuxt

In Nuxt, you typically wrap <RouterView /> or <NuxtPage /> to cache entire route components. This is powerful for multi-page dashboards.

typescript
// app.vue or layout
<template>
  <div class="layout">
    <nav><!-- navigation --></nav>

    <KeepAlive :include="cachedRoutes" :max="10">
      <NuxtPage />
    </KeepAlive>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// Cache only these route components
const cachedRoutes = ref([
  'ProductListPage',
  'DashboardPage',
  'AnalyticsPage',
])
</script>

Each page component should have a matching name to work with include. When users navigate between these pages, the component tree is cached. They return to the exact state they left—scroll position, filters applied, form drafts—all intact. A typical trading or e-commerce team reports 20-30% faster perceived navigation with this pattern.

The onActivated and onDeactivated Lifecycle Hooks

When a component is cached and reactivated, it doesn't re-run setup() or onMounted(). Instead, it fires onActivated. Similarly, when it's deactivated (swapped out but kept alive), onDeactivated fires.

Use these hooks to manage subscriptions, polls, or listeners:

typescript
<script setup lang="ts">
import { ref, onActivated, onDeactivated } from 'vue'

const data = ref([])
let pollInterval: NodeJS.Timeout | null = null

// Start polling only when active
onActivated(() => {
  console.log('Reactivated, resuming poll')
  pollInterval = setInterval(fetchData, 5000)
  fetchData() // Immediate fetch
})

// Pause polling when inactive to save resources
onDeactivated(() => {
  console.log('Deactivated, pausing poll')
  if (pollInterval) {
    clearInterval(pollInterval)
    pollInterval = null
  }
})

const fetchData = async () => {
  const res = await fetch('/api/data')
  data.value = await res.json()
}

// On first mount, also start polling
onMounted(fetchData)
</script>

This pattern is crucial. If you cache a component with a WebSocket listener or a polling interval without pausing it on deactivation, you'll have multiple subscribers running in the background, burning CPU and memory. On a dashboard with 5 cached widgets each polling an API, this bloats the backend and client. Using onDeactivated to clean up is not optional.

Limiting Cache with the max Prop

By default, KeepAlive caches unlimited components. On a router with dozens of pages or a component switcher that cycles through many options, you can exhaust memory quickly. Use max to set a cap. When the limit is reached, the least recently used component is evicted.

typescript
<template>
  <!-- Cache a maximum of 5 components -->
  <KeepAlive :max="5" include="TabA,TabB,TabC">
    <component :is="getComponent(activeTab)" :key="activeTab" />
  </KeepAlive>
</template>

Combined with include, this is your safety valve. If you're caching 10 heavy dashboard pages but only 5 fit in memory, the oldest unused one gets destroyed. A user revisits that page later, and it re-mounts—a minor performance dip, but your memory stays controlled.

On a ride-hailing app where drivers can view multiple trip details, we'd set :max="3". A driver can keep 3 trip details cached and swiftly jump between them; the 4th one loads fresh on click. Performance and UX remain snappy without runaway memory growth.

Real-World Scenario: Multi-Step Form Wizard

Imagine a SaaS onboarding flow: step 1 collects company info, step 2 gathers team members, step 3 handles billing. Each step is a heavy component with API calls and form validation.

typescript
// wizard.vue
<template>
  <div class="wizard">
    <div class="steps">
      <button
        v-for="step in steps"
        :key="step"
        @click="currentStep = step"
        :class="{ active: currentStep === step }"
      >
        Step {{ step }}
      </button>
    </div>

    <KeepAlive>
      <component
        :is="getStepComponent(currentStep)"
        :key="currentStep"
        @next="currentStep++"
        @prev="currentStep--"
      />
    </KeepAlive>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import StepOne from './StepOne.vue'
import StepTwo from './StepTwo.vue'
import StepThree from './StepThree.vue'

const currentStep = ref(1)
const steps = [1, 2, 3]

const getStepComponent = (step: number) => {
  const map = { 1: StepOne, 2: StepTwo, 3: StepThree }
  return map[step]
}
</script>

Without KeepAlive, clicking between steps would reset the form each time. With KeepAlive, user data is preserved. They fill step 1, advance to step 2, change their mind, click back—step 1's form data is exactly as they left it. No re-fetching, no validation re-running. The wizard feels instantaneous.

Common Mistakes

  • Forgetting component names: include and exclude match the component's name option, not the filename. Missing defineOptions({ name: 'MyComponent' }) means your filtering won't work.
  • Not cleaning up in onDeactivated: Leaving timers, listeners, or subscriptions running in cached components creates memory leaks and multiple concurrent subscriptions. Always pair onActivated with onDeactivated for resource management.
  • Caching everything by default: Memory bloats when you cache every component. Be intentional with include or exclude. Cache only components that benefit from preservation.
  • Ignoring the max prop: On large apps or routers with many pages, skipping max can lead to unbounded memory growth. Set a reasonable cap based on your app's needs.
  • Using non-stable keys: If :key changes on every render (e.g., :key="Math.random()"`), KeepAlive can't recognize the component and won't reuse the cache. Keep keys stable and meaningful.
  • Caching components with critical side effects: If a component's mount side effect is essential (analytics, auth checks, data sync), caching might skip it. Use onActivated to re-trigger if needed.

Key Takeaways

  • KeepAlive preserves component state between switches, eliminating re-initialization and re-fetching.
  • Wrap dynamic components or router views in <KeepAlive> to cache them.
  • Use include and exclude with component names to filter what gets cached.
  • Use onActivated and onDeactivated to manage subscriptions, polls, and resource cleanup—this prevents memory leaks.
  • Set :max to cap the cache size and prevent runaway memory growth.
  • Be intentional: cache only components that benefit (heavy forms, filtered lists, charts) and exclude lightweight or one-time components.
  • Test in DevTools to verify cache hits and ensure your include/exclude` filters work as expected.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles