In Vue projects, some components or pages do not need to be rendered repeatedly. In those cases, it is useful to keep part of the UI alive in memory. This is not just simple data persistence. It is persistence of the entire component instance, including both state and view structure.
Vue provides the built-in <keep-alive> component for exactly this purpose.
When <keep-alive> wraps dynamic components, inactive component instances are cached instead of destroyed. Like <transition>, it is an abstract component: it does not render a DOM element of its own and does not appear in the parent chain the way a normal component does.
When a component is switched inside <keep-alive>, the activated and deactivated lifecycle hooks will run.
There are two common eras of usage.
Before Vue 2.1.0, people often used a pattern like this:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
new Router({
routes: [
{
name: "a",
path: "/a",
component: A,
meta: {
keepAlive: true,
},
},
{
name: "b",
path: "/b",
component: B,
},
],
});After configuring route meta like this, route a gets cached while route b does not. Any route that should be cached can simply add keepAlive: true.
After Vue 2.1.0, <keep-alive> introduced two useful props:
include: only matching components are cachedexclude: matching components are not cached, and this takes priority over includeBoth props support:
When using regexes or arrays, v-bind is required.
Examples:
<!-- comma-separated string -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- regular expression -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- array -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
<!-- with router-view -->
<template>
<div id="app">
<transition :name="routerTransition">
<keep-alive :include="keepAliveComponentsData">
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
</transition>
</div>
</template>The post-2.1.0 approach is cleaner and gives you more control over caching strategy.
Basic usage is only the beginning. Real projects often need a more intentional caching strategy. The key question becomes: how can components dynamically decide whether they should be cached?
Imagine this flow:
This is common in mobile applications.
One workaround is to pass flags around and decide whether to use store data or refetch fresh data. That works, but it becomes awkward and does not provide real page-level caching.
If you use keep-alive carefully, the flow becomes much cleaner.
setKeepAlivesetNoKeepAlivegetKeepAlivename of every component and store them in an arrayApp.vue mountsApp.vue and sync them to includeconst state = {
keepAliveComponents: [],
};
const getters = {
getKeepAlive(state) {
return state.keepAliveComponents;
},
};
const mutations = {
setKeepAlive(state, component) {
!state.keepAliveComponents.includes(component) &&
state.keepAliveComponents.push(component);
},
setNoKeepAlive(state, component) {
const index = state.keepAliveComponents.indexOf(component);
index !== -1 && state.keepAliveComponents.splice(index, 1);
},
};
const actions = {};
export default {
state,
getters,
mutations,
actions,
};routes.forEach((item) => {
store.commit("setKeepAlive", item.name);
});
export default routes;Note: this uses item.name instead of item.component.name.
In principle, include matches component names. But when code-splitting and bundling are involved, component names can change unexpectedly. In practice, it is safer to keep route names and component names aligned from the start.
App.vue <template>
<div id="app">
<transition :name="routerTransition">
<keep-alive :include="keepAliveComponentsData">
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
</transition>
</div>
</template>
<script>
import store from "./store";
export default {
name: "App",
data() {
return {
keepAliveComponentsData: [],
};
},
mounted() {
this.keepAliveComponentsData = store.getters.getKeepAlive;
},
};
</script>If you jump from a detail page into a row-detail page, you may not want the row-detail page cached:
toLoanApplicationDetail(index) {
store.commit("setNoKeepAlive", "LoanlineReadonly");
}If you then jump from row detail into another associated document and want to preserve the row detail page, you can cache it again:
toContractDetail(item) {
store.commit("setKeepAlive", "LoanlineReadonly");
}Now you need to keep the include list in sync with the store:
<template>
<div id="app">
<transition :name="routerTransition">
<keep-alive :include="keepAliveComponentsData">
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
</transition>
</div>
</template>
<script>
import store from "./store";
export default {
name: "App",
data() {
return {
keepAliveComponentsData: [],
};
},
mounted() {
this.keepAliveComponentsData = store.getters.getKeepAlive;
},
watch: {
$route() {
this.keepAliveComponentsData = store.getters.getKeepAlive;
},
},
};
</script>The core idea of keep-alive is to cache components as VNodes and reuse them when they match the include / exclude rules.
If the rules change and a cached VNode no longer matches, it gets destroyed.
At a high level:
created initializes a cache object to hold cached VNodesdestroyed clears cached component instancesrender: include / excludeinclude and exclude prune the cache when rules changecreated builds the cache containerrender decides whether to reuse or cache the current component vnodeinclude and exclude determine which names are allowed to stay cachedThat is why keep-alive can preserve component state and view state so effectively.