Qué es KeepAlive y cuándo usarlo
KeepAlive es un componente built-in de Vue que cachea las instancias de componentes dinámicos. Cuando un componente se desmonta normalmente, su estado interno se destruye y debe reconstruirse al volver a montarse. KeepAlive evita esta destrucción: mantiene la instancia del componente en memoria y la reactiva cuando vuelve a mostrarse.
Los casos de uso principales son:
- Interfaces de pestañas: Al cambiar entre pestañas, el usuario espera que el estado de cada pestaña se preserve (formularios a medio completar, posición de scroll, etc.)
- Componentes dinámicos: Cuando se usa
<component :is>para alternar entre componentes - Rutas con caché: Al navegar entre rutas y querer mantener el estado de páginas visitadas anteriormente
- Formularios multi-paso: Para preservar los datos introducidos al navegar entre pasos
Cacheado de componentes dinámicos
Sin KeepAlive, un componente dinámico se destruye al cambiar a otro:
<script setup lang="ts">
import { shallowRef } from 'vue'
import TabHome from './TabHome.vue'
import TabProfile from './TabProfile.vue'
import TabSettings from './TabSettings.vue'
const tabs = [
{ name: 'Inicio', component: TabHome },
{ name: 'Perfil', component: TabProfile },
{ name: 'Ajustes', component: TabSettings }
] as const
const currentTab = shallowRef(tabs[0].component)
</script>
<template>
<div class="tab-buttons">
<button
v-for="tab in tabs"
:key="tab.name"
:class="{ active: currentTab === tab.component }"
@click="currentTab = tab.component"
>
{{ tab.name }}
</button>
</div>
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
</template>
Al envolver <component :is> con KeepAlive, los componentes TabHome, TabProfile y TabSettings se cachean al desactivarse. Si el usuario introduce datos en un formulario de TabSettings, navega a TabHome y vuelve a TabSettings, los datos del formulario siguen presentes.
shallowRef es la opción recomendada para almacenar componentes dinámicos, ya que evita la conversión profunda a reactivo que ref aplicaría sobre el objeto componente.
Props include y exclude
Las props include y exclude controlan qué componentes se cachean y cuáles no. Aceptan tres formatos:
String con nombres separados por comas
<KeepAlive include="TabHome,TabProfile">
<component :is="currentTab" />
</KeepAlive>
Expresión regular
<KeepAlive :include="/^Tab/">
<component :is="currentTab" />
</KeepAlive>
Array de strings
<KeepAlive :include="['TabHome', 'TabProfile']">
<component :is="currentTab" />
</KeepAlive>
Los nombres deben coincidir con la opción name del componente. En componentes con <script setup>, Vue infiere el nombre del archivo (por ejemplo, TabHome.vue produce el nombre TabHome).
exclude funciona de la misma manera pero invierte la lógica: los componentes especificados no se cachean:
<KeepAlive :exclude="['TabSettings']">
<component :is="currentTab" />
</KeepAlive>
En este caso, TabHome y TabProfile se cachean, pero TabSettings se destruye y recrea cada vez.
Límite de caché con max
La prop max establece un número máximo de instancias cacheadas. Cuando se alcanza el límite, la instancia cacheada que lleva más tiempo sin accederse se destruye para hacer espacio (estrategia LRU - Least Recently Used):
<KeepAlive :max="5">
<component :is="currentTab" />
</KeepAlive>
Esto es útil en aplicaciones con muchas vistas o componentes dinámicos para evitar un consumo excesivo de memoria. Con :max="5", se mantienen como máximo 5 instancias en caché, destruyendo la menos recientemente usada cuando se supera el límite.
Hooks onActivated y onDeactivated
Los componentes cacheados por KeepAlive no disparan onMounted ni onUnmounted al activarse o desactivarse. En su lugar, Vue proporciona dos hooks específicos:
onActivated: Se ejecuta cuando un componente cacheado se vuelve a mostrar (también en el primer montaje)onDeactivated: Se ejecuta cuando un componente cacheado se oculta (en lugar de destruirse)
<!-- TabProfile.vue -->
<script setup lang="ts">
import { ref, onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'
const lastActivatedAt = ref('')
onMounted(() => {
console.log('TabProfile: montado (solo la primera vez)')
})
onUnmounted(() => {
console.log('TabProfile: desmontado (solo si KeepAlive lo expulsa del caché)')
})
onActivated(() => {
lastActivatedAt.value = new Date().toLocaleTimeString()
console.log('TabProfile: activado')
// Recargar datos, reiniciar timers, reconectar websockets...
})
onDeactivated(() => {
console.log('TabProfile: desactivado')
// Pausar timers, desconectar websockets, limpiar recursos...
})
</script>
<template>
<div class="profile-tab">
<h2>Perfil de usuario</h2>
<p>Última activación: {{ lastActivatedAt }}</p>
</div>
</template>
Casos de uso comunes para estos hooks:
onActivated: Refrescar datos que pueden haber cambiado, reiniciar intervalos o timers, reconectar websockets, restaurar la posición de scrollonDeactivated: Pausar o limpiar intervalos, desconectar websockets, guardar el estado de scroll
KeepAlive con Vue Router
KeepAlive se usa con Vue Router envolviendo el componente <RouterView>:
<template>
<RouterView v-slot="{ Component }">
<KeepAlive :include="cachedRoutes" :max="10">
<component :is="Component" />
</KeepAlive>
</RouterView>
</template>
<script setup lang="ts">
const cachedRoutes = ['HomeView', 'ProductListView']
</script>
En este ejemplo, las vistas HomeView y ProductListView se cachean al navegar a otra ruta, preservando su estado. Otras vistas como ProductDetailView se destruyen normalmente.
Combinación con Transition
Se puede combinar KeepAlive con Transition para animar los cambios de ruta:
<template>
<RouterView v-slot="{ Component }">
<Transition name="fade" mode="out-in">
<KeepAlive :include="['HomeView', 'DashboardView']">
<component :is="Component" :key="$route.fullPath" />
</KeepAlive>
</Transition>
</RouterView>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
El orden es importante: Transition debe envolver a KeepAlive, no al revés.
Ejemplo práctico: interfaz de pestañas con estado
<!-- TabContainer.vue -->
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import SearchTab from './SearchTab.vue'
import FormTab from './FormTab.vue'
import ListTab from './ListTab.vue'
interface Tab {
id: string
label: string
component: any
}
const tabs: Tab[] = [
{ id: 'search', label: 'Buscar', component: SearchTab },
{ id: 'form', label: 'Formulario', component: FormTab },
{ id: 'list', label: 'Listado', component: ListTab }
]
const activeTab = ref('search')
const activeComponent = shallowRef(SearchTab)
function switchTab(tab: Tab) {
activeTab.value = tab.id
activeComponent.value = tab.component
}
</script>
<template>
<div class="tab-container">
<nav class="tab-nav">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: activeTab === tab.id }]"
@click="switchTab(tab)"
>
{{ tab.label }}
</button>
</nav>
<div class="tab-content">
<KeepAlive :max="3">
<component :is="activeComponent" :key="activeTab" />
</KeepAlive>
</div>
</div>
</template>
<style scoped>
.tab-container {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.tab-nav {
display: flex;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.tab-btn {
flex: 1;
padding: 12px 16px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
color: #666;
transition: all 0.2s;
}
.tab-btn.active {
background: white;
color: #333;
font-weight: bold;
border-bottom: 2px solid #3498db;
}
.tab-content {
padding: 24px;
}
</style>
<!-- SearchTab.vue -->
<script setup lang="ts">
import { ref, onActivated, onDeactivated } from 'vue'
const query = ref('')
const results = ref<string[]>([])
function search() {
results.value = [
`Resultado para "${query.value}" - 1`,
`Resultado para "${query.value}" - 2`,
`Resultado para "${query.value}" - 3`
]
}
onActivated(() => {
console.log('SearchTab activado - la búsqueda anterior se preserva')
})
onDeactivated(() => {
console.log('SearchTab desactivado - estado guardado en caché')
})
</script>
<template>
<div>
<h3>Búsqueda</h3>
<div class="search-bar">
<input v-model="query" placeholder="Buscar..." @keyup.enter="search" />
<button @click="search">Buscar</button>
</div>
<ul v-if="results.length > 0">
<li v-for="(result, i) in results" :key="i">{{ result }}</li>
</ul>
<p v-else class="hint">Escribe algo y pulsa buscar</p>
</div>
</template>
<!-- FormTab.vue -->
<script setup lang="ts">
import { ref, onActivated } from 'vue'
const name = ref('')
const email = ref('')
const message = ref('')
onActivated(() => {
console.log('FormTab activado - formulario preservado')
})
</script>
<template>
<div>
<h3>Formulario de contacto</h3>
<form @submit.prevent>
<div class="field">
<label>Nombre</label>
<input v-model="name" />
</div>
<div class="field">
<label>Email</label>
<input v-model="email" type="email" />
</div>
<div class="field">
<label>Mensaje</label>
<textarea v-model="message" rows="4"></textarea>
</div>
<button type="submit">Enviar</button>
</form>
</div>
</template>
En este ejemplo completo, el usuario puede buscar algo en la pestaña de búsqueda, empezar a rellenar el formulario de contacto, cambiar de pestaña y volver sin perder ningún dato. KeepAlive con :max="3" asegura que todas las pestañas se cacheen mientras haya espacio, y los hooks onActivated/onDeactivated permiten ejecutar lógica al cambiar entre ellas.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje