Skip to content

Hot reload doesn't drop the old component if it's wrapped in <keep-alive> #700

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
pevzi opened this issue Mar 9, 2017 · 7 comments
Open

Comments

@pevzi
Copy link

pevzi commented Mar 9, 2017

(not sure if I've chosen the appropriate bug tracker for this)

Steps to reproduce:

  1. Create an empty Vue project with the webpack template.
  2. Open App.vue and wrap the <hello> component in a <keep-alive>.
  3. Run the dev server.
  4. Modify the script section of Hello component (modifying the template doesn't trigger the issue).
  5. See Components tab in vue-devtools.

Expected behavior: the wrapped component is replaced with the new version.
Observed behavior: the old version is kept alive, and each change results in an additional inactive component.

@LinusBorg
Copy link
Member

Would you mind creating a small reproduction so we can be sure the behaviour can be replicated in isolation?

Thanks!

@pevzi
Copy link
Author

pevzi commented Apr 20, 2017

Here is a small project based on webpack-simple template. Just do npm run dev and try to change the msg field in src/Foo.vue.

@LinusBorg
Copy link
Member

Thanks!

@ericwu-wish
Copy link

ericwu-wish commented May 27, 2020

for vue 2.6, i did some work around, this will require name to be set in component:

import Vue from "vue"
/*
* https://github.com/vuejs/vue-loader/issues/1332#issuecomment-601572625
*/
function isDef(v) {
    return v !== undefined && v !== null
}
function isAsyncPlaceholder(node) {
    return node.isComment && node.asyncFactory
}
function getFirstComponentChild(children) {
    if (Array.isArray(children)) {
    for (var i = 0; i < children.length; i++) {
        var c = children[i]
        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
        return c
        }
    }
    }
}
function getComponentName(opts) {
    return opts && (opts.Ctor.options.name || opts.tag)
}
function matches(pattern, name) {
    if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
    } else if (typeof pattern === "string") {
    return pattern.split(",").indexOf(name) > -1
    } else if (isRegExp(pattern)) {
    return pattern.test(name)
    }
    /* istanbul ignore next */
    return false
}
function remove(arr, item) {
    if (arr.length) {
    var index = arr.indexOf(item)
    if (index > -1) {
        return arr.splice(index, 1)
    }
    }
}
function pruneCacheEntry(cache, key, keys, current) {
    var cached$$1 = cache[key]
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
    cached$$1.componentInstance.$destroy()
    }
    cache[key] = null
    remove(keys, key)
}
function pruneCache(keepAliveInstance, filter) {
    var cache = keepAliveInstance.cache
    var keys = keepAliveInstance.keys
    var _vnode = keepAliveInstance._vnode
    const cachedNameKeyMap = keepAliveInstance.cachedNameKeyMap
    for (var key in cache) {
    var cachedNode = cache[key]
    if (cachedNode) {
        var name = getComponentName(cachedNode.componentOptions)
        if (name && !filter(name)) {
        delete cachedNameKeyMap[name]
        pruneCacheEntry(cache, key, keys, _vnode)
        }
    }
    }
}
const patternTypes = [String, RegExp, Array]
const KeepAlive = {
  name: "keep-alive",
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number],
  },

  created() {
    this.cache = Object.create(null)
    this.cachedNameKeyMap = Object.create(null)
    this.keys = []
  },
  destroyed() {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },
  mounted() {
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
  render() {
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    const componentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }
      const { cache, cachedNameKeyMap, keys } = this
      const key =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        // prune old component for hmr
        if (name && cachedNameKeyMap[name] && cachedNameKeyMap[name] !== key) {
          pruneCacheEntry(cache, cachedNameKeyMap[name], keys)
        }
        cachedNameKeyMap[name] = key
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  },
}
// ovveride original keep-alive
process.env.NODE_ENV === "development" && Vue.component("KeepAlive", KeepAlive)

@dwatts3624
Copy link

@ericwu-wish thanks for sharing the example! Being able to work around the HMR problem without disabling keep-alive entirely in dev would definitely shore up a ton of confusing code we've had to write inside components to determine if keep-alive is being used or not since we have different behaviors in certain cases within created/destroyed (e.g. maybe we only refresh a small amount of data when the component is activated).

Are you still using this? I tried implementing it and have confirmed that the code is running after some debugging but my components still disappear when HMR runs.

I took over this project which was setup using Laravel Mix so I suspect I need to dig into the way it's configuring webpack and the versions it's using but figured I'd verify that I should expect HMR to not wipe out my components on reload when using your overridden version of keep-alive here.

Any advice?

@dwatts3624
Copy link

I posted this in #1332 but wanted to share here.

I ended up rolling the comments from @nailfar & @ericwu-wish into a plugin:
https://www.npmjs.com/package/vue-keep-alive-dev

As mentioned on the other thread, I'm still wondering if it might be easier just to always append the value of componentOptions.Ctor.cid to the cache key. This is a lot of extra code for what is ultimately a small modification to a single line which doesn't have any negative affect in production.

@Drumstix42
Copy link

It's a shame this issue still exists. I've just run into the bug as well.
Do we know if this will be fixed/addressed in Vue 3?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants