Reputation: 1647
My Vue app has a dynamic tabs mechanism.
Users can create as many tabs as the want on the fly, each tab having its own state (eg "Pages").
I am using the <keep-alive>
component to cache the different pages.
<keep-alive include="page">
<router-view :key="$route.params.id" />
</keep-alive>
But users can also "close" individual tab. As pages tend to store a lot of datas, I would like to delete the according page component from the cache, as the user close the tab.
How can I programmatically destroy a cached component inside keep-alive ?
Upvotes: 14
Views: 15581
Reputation: 846
For vue3
+ vite
Patch the source code with vite.config.ts
function patchVueKeepAlive() {
let replaced = false
return {
name: 'patch-vue-keep-alive',
// https://cn.rollupjs.org/plugin-development/#transform
transform(code, id) {
if (!replaced && id.includes('/node_modules/')) {
// make the function accessable from outside
code = code.replace(/function pruneCacheEntry\(key\) (\{\s+const cached = cache\.get\(key\);.+?keys.delete\(key\);\s+})/s, (...ms) => {
replaced = true
return 'var pruneCacheEntry = sharedContext.prune = function(key)' + ms[1]
})
if (replaced) {
console.log('Keep-Alive Hacked!')
return {code, map: null}
}
}
}
}
}
const viteConfig = defineConfig((mode: ConfigEnv) => {
return {
plugins: [patchVueKeepAlive(), /* ... */ ],
}
}
npm/yarn dev --force
Now prune()
on keep-alive ref
is usable
<keep-alive ref="KeepAlive">
<component :is="Component" :key="reloadKey" />
</keep-alive>
const KeepAlive = ref()
router.afterEach(() => {
let key
while (key = pruneQueue.pop()) {
console.log('prune', key)
try {
KeepAlive.value.prune(key)
} catch (e: any) {
// cache not exists
console.log(e)
}
}
// show all the caches
console.log(KeepAlive.value.$.__v_cache)
})
const pruneQueue: string[] = []
function pruneCache(key: string) {
pruneQueue.push(key)
}
Upvotes: 0
Reputation: 1
Based on the solutions provided by @feasin and @Alex, I've implemented a method to clear cached views. Below is the approach that I've taken:
<template>
<div>
<RouterView v-slot="{ Component }">
<KeepAlive :include="whitelistedViews">
<component :is="Component" />
</KeepAlive>
</RouterView>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from "vue"
// Initialize whitelistedViews with a ref holding an undefined value
// This will initially allow all views to be cached.
const whitelistedViews = ref<string[]>() // Ref<string[] | undefined>
// Function to clear cached views
async function clearCachedViews() {
// First, set the value to an empty array to clear all current caches
whitelistedViews.value = []
// Wait for the next DOM update cycle to ensure the cache is cleared
await nextTick()
// Finally, reset whitelistedViews to undefined to allow all views to be cached again
whitelistedViews.value = undefined
}
</script>
You can also use provide/inject to share the clearCachedViews
function with child components.
Upvotes: 0
Reputation: 161
There is no built-in function in keep-alive
which allows you to clear a specific component from the cache.
However, you can clear the cache from the VNode directly inside the component you want to destroy by calling this function :
import Vue, { VNode } from 'vue'
interface KeepAlive extends Vue {
cache: { [key: string]: VNode }
keys: string[]
}
export default Vue.extend({
name: 'PageToDestroy',
...
methods: {
// Make sure you are not on this page anymore before calling it
clearPageFromKeepAlive() {
const myKey = this.$vnode.key as string
const keepAlive = this.$vnode.parent?.componentInstance as KeepAlive
delete keepAlive.cache[myKey]
keepAlive.keys = keepAlive.keys.filter((k) => k !== myKey)
this.$destroy()
}
},
})
For me, it doesn't cause any memory leaks and the component is not in the Vue.js devtools anymore.
Upvotes: 2
Reputation: 5227
based on the answer of @feasin, here is the setup I am using
template
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
script
import { ref, inject, watch } from "vue";
export default {
components: { CustomRouterLink },
setup() {
const cachedViewsDefault = ["Page1", "Page1", "Page3"];
var cachedViews = ref([]);
const auth = inject("auth");
// check whether user is logged in (REACTIVE!)
const isSignedIn = auth.isSignedIn;
// set the initial cache state
if (isSignedIn.value) {
cachedViews.value = cachedViewsDefault;
}
// clear the cache state
watch(isSignedIn, () => {
if (!isSignedIn.value) {
cachedViews.value = [];
} else {
cachedViews.value = cachedViewsDefault;
}
});
return {
cachedViews,
};
},
};
First I set the initial cached views value based on whether the user is logged in or not.. After the user logs-out I simply set the array value to an empty array. When the user logs back in - I push the default array keys back into the array.
This example of course does not provide the login/logout functionality, it is only meant as a POC to to the solution proposed by the @feasin (which seems like a good approach to me)
Edit 19.01.2022
I now understand the shortcomings of such approach. It does not allow to gradually destroy a certain component. Given that we have a component named Item
and it's path is Item/{id}
- there is currently no native way (in Vuejs3
) to remove, let's say a cached item
with Id = 2
. Follow up on this issue on the Github
: https://github.com/vuejs/rfcs/discussions/283
Edit 20-21.01.2022
Note that you have to use the computed
function for inclusion list. Otherwise the component will not ever be unmounted
.
Here is the fiddle with the problem: https://jsfiddle.net/7f2d4c0t/4/
Here's fiddle with the fix: https://jsfiddle.net/mvj2z3pL/
return {
cachedViews: computed(() => Array.from(cachedViews.value)),
}
Upvotes: 2
Reputation: 31
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
cachedViews is the array of the route component name
First when create a tab, cachedViews push the cached route name, when you switch the opened tab, the current route is cached.
Second when close the tab, cachedViews pop the cached route name, the route component will destroyed.
Upvotes: 3
Reputation: 2523
You can call this.$destroy()
before user close the tab and delete all of data and event binding in that one.
Upvotes: 11
Reputation: 4233
If you don't mind losing the state when a tab is added/removed, then you can try these:
Upvotes: 3