Thomas
Thomas

Reputation: 1647

How can I destroy a cached Vue component from keep-alive?

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

Answers (7)

Time Killer
Time Killer

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

su-rin
su-rin

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

Florian Zdrada
Florian Zdrada

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

Alex
Alex

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

feasin
feasin

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

bcjohn
bcjohn

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

Teddy
Teddy

Reputation: 4233

If you don't mind losing the state when a tab is added/removed, then you can try these:

  • Use v-if and turn off the keep-alive component and turn it back on in nextTick
  • Use v-bind on the include list, and remove "page" and add it back in nextTick

Upvotes: 3

Related Questions