How to programmatically destroy a Vue3 component instance?

Viewed 1918

I am working on a Vue3 plugin that generates a toast after some user action on the UI.

I create the toast with createVNode and render and this works great. After 2500ms I would like the toast to disappear from the UI and the Vue component Toast to be destroyed. However I noticed that render(null, container) or document.body.removeChild(container) does actually remove the Toast from the UI and the Toast node from the DOM, it does not destroy the Toast component instance. My concern is that after dozens of Toasts, memory will increase without ever be cleared from the old Toast.

Is there a way in Vue3 to programmatically destroy a component ?

import { createVNode, Plugin, render } from 'vue'
import Alert from './Alert.vue'

const plugin: Plugin = {
    // eslint-disable-next-line
    install(app, options) {

        const toast = {
            show(message: string) {

                const container = document.createElement('div')
                document.body.appendChild(container)

                const toastVNode = createVNode(
                    Alert,
                    { message: message },
                    null
                )

                render(toastVNode, container)

                setTimeout(function () {
                    render(null, container)
                    document.body.removeChild(container)
                }, 2500);

            }
        }

        app.provide("$Toast", toast);
    }
}

export default plugin
2 Answers

Instead of createVNode() and render() (internal functions), use createApp() and mount() from the public API. createApp returns an application instance, which provides unmount() to destroy the mounted component:

import { createApp } from 'vue'

//...

const container = document.createElement('div')
document.body.appendChild(container)

let toastApp = createApp(Alert, { message })
toastApp.mount(container)

setTimeout(() => {
  toastApp.unmount()
  toastApp = undefined
  document.body.removeChild(container)
}, 2500)

Alert component should accept duration/closeDelay as prop, and on it's mounted hook, call the setTimeout handler and emit some custom events let's call it closeEvent . if using vue2 use this.$emit("closeEvent") or if using vue 3 use context.$emit("closeEvent"). If Alert is component from a a third party ui library, then you can create a wrapper component around it

Your function should now look like this

const container = document.createElement('div')

document.body.appendChild(container)

let toastApp = createApp(Alert,{closeDelay:2500,message:message },null)

//Notice I create a listener for closeEvent with toastApp.props.onCloseEvent before

toastApp.props.onCloseEvent=()=>{
 render(null,container);
}

//Made this as the last call
render(toastApp,container);

On your Alert component, if using vue 3, your code should look close to something like

<script> 
 export default {
 props:["message","closeDelay"],
 emits: ["closeEvent"],
 setup(props,context){
 onMounted(()=>{
    setTimeout(() => {
    context.emit("closeEvent")
   }, props.closeDelay)
   })
  }
 }
</script>
Related