How to add Vue lifecycle listener dynamically

You might ask, can it be simpler?

From Vue.js Component Hooks as Events, this is the syntax you are looking for

this.$once('hook:beforeDestroy', () => {

I'm not sure how exactly you intend to make it dynamic, but here is an adaption of your logOnDestroy() method in Vue CLI's default HelloWorld app,

Demo

Vue.component('helloworld', {
  template: '<h1>{{ msg }}</h1>',
  name: 'helloworld',
  props: { msg: String },
  mounted() {
    this.logOnDestroy('Goodbye HelloWorld')
  },
  methods: {
    logOnDestroy(txt) {
      this.$once('hook:beforeDestroy', () => {
        console.log(txt)
      })
    }    
  }
});

new Vue({
  el: '#app',
  data: {
    showHello: true
  },
  mounted() {
    setTimeout(() => {
      this.showHello = false
    }, 3000)
  }
});
Vue.config.productionTip = false
Vue.config.devtools = false
<script src="https://unpkg.com/vue"></script>
<div id="app">
  <img alt="Vue logo" src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/277px-Vue.js_Logo_2.svg.png" style="height: 50px;">
  <helloworld v-if="showHello" msg="Welcome to Your Vue.js App"/>
</div>


Jan 2019 - A warning about the answer to this question - I found a subtle problem when using this code to add dynamic listeners.

this.$options.beforeDestroy.push(() => {
  console.log(txt)
});

It works ok when there is no static beforeDestroy defined. In this case the handlers array is a direct property of $options.

But if you define a static beforeDestroy hook on the component, the handlers array is property of $options.__proto__, which means multiple instances of the component inherit dynamic handlers of previous instances (effectively, the above code modifies the template used to create successive instances).

How much of a practical problem this is, I'm not sure. It looks bad because the handler array gets bigger as you navigate the app (e.g switching pages adds a new function each time).


A safer way of adding dynamic handlers is to use this injectHook code, which is used by Vue for hot module reload (you can find it in index.js of a running Vue app). Note, I am using Vue CLI 3.

function injectHook(options, name, hook) {
  var existing = options[name]
  options[name] = existing
    ? Array.isArray(existing) ? existing.concat(hook) : [existing, hook]
    : [hook]
}
...
injectHook(this.$options, 'beforeDestroy', myHandler)

What happens here is a new array is created on the instance which contains all the handlers from __proto__, plus the new one. The old array still exists (unmodified), and the new array is destroyed along with the instance, so there is no build-up of handlers in __proto__ handler array.


An array of handlers for each life-cycle event is stored in the this.$options object. You can add a handler by pushing to the corresponding array (you'll need to create the array first if there are no handlers set already):

new Vue({
  el: '#app',
  created() {
    if (!this.$options.mounted) {
      this.$options.mounted = [];
    }
  
    this.$options.mounted.push(() => {
      console.log('mounted')
    });
    
    this.$options.mounted.push(() => {
      console.log('also mounted')
    });
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>

<div id="app">
  <div></div>
</div>

So in your case:

methods: {
  logOnDestroy(txt) {
    if (!this.$options.beforeDestroy) {
      this.$options.beforeDestroy = [];
    }

    this.$options.beforeDestroy.push(() => {
      console.log(txt)
    });
  }
}