Dynamically instantiating a component in Vue.js

Viewed 6583

Following this tutorial, I'm trying to programmatically create instances of a component on my page.

The main snippet is this:

import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass()
instance.$mount()
this.$refs.container.appendChild(instance.$el)

However I get two errors:

  1. The component I'm trying to instantiate contains references to the store, and these don't work: "TypeError: Cannot read property 'state' of undefined".

  2. For the last line of the snippet (this.$refs.container.appendChild(instance.$el)) I get this error: "Uncaught TypeError: Cannot read property 'container' of undefined"

I'm really not sure how to troubleshoot this, if anyone strong in Vue.js could give me some hint as to why I'm getting these errors and to solve them that would be terrific.

4 Answers

1) Since you're manually instantiating that component and it doesn't belong to your main app's component tree, the store won't be automatically injected into it from your root component. You'll have to manually provide the store to the constructor when you instantiate the component ..

import ProjectRow from "./ProjectRow.vue";
import Vue from "vue";
import store from "../store";

let ProjectRowClass = Vue.extend(ProjectRow);
let ProjectRowInstance = new ProjectRowClass({ store });

2) In a Vue Single File Component (SFC), outside of the default export this doesn't refer to the Vue instance, so you don't have access to $refs or any other Vue instance property/method. To gain access to the Vue instance you'll need to move this line this.$refs.container.appendChild(instance.$el) somewhere inside the default export, for example in the mounted hook or inside one of your methods.

See this CodeSandbox for an example of how you may go about this.

It works by assigning "this" to the property "parent". By setting the parent you also have access to the $store in the new instance. (Provided that "this" is another Vue instance/Component and already has access to the store, of course)

new (Vue.extend(YourNewComponent))({
    parent: this,
    propsData: {
        whatever: 'some value',
    },
}).$mount(el.querySelector('.some-id'))

If you don't need the reference to the parent, you can just leave "parent: this," out.

Important note: When mounting many (like 500+) items on the page this way you will get a huge performance hit. It is better to only give the new Component the necessary stuff via props instead of giving it the entire "this" object.

This is another way to instantiate a component in Vue.js, you can use two different root elements.

// Instantiate you main app
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

//
// Then instantiate your component dynamically
//

// Create a component or import it.
const Hello = {
  props: ['text'],
  template: '<div class="hello">{{ text }}</div>',
};

// Create a componentClass by Vue.
const HelloCtor = Vue.extend(Hello);

// Use componentClass to instantiate your component.
const vm = new HelloCtor({
  propsData: {
    text: 'HI :)'
  }
})
// then mount it to an element.
.$mount('#mount');


I went down this path, following all the examples above, and even this one: https://css-tricks.com/creating-vue-js-component-instances-programmatically/

While I got far, and it works (I made a lot of components this way), at least for my case, it came with drawbacks. For example I'm using Vuetify at the same time, and the dynamically added components didn't belong to the outer form, which meant that while local (per component) validation worked, the form didn't receive the overall status. Another thing that did not work was to disable the form. With more work, passing the form as parent property, some of that got working, but what about removing components. That didn't go well. While they were invisible, they were not really removed (memory leak).

So I changed to use render functions. It is actually much easier, well documented (both Vue 2 and Vue 3), and everything just works. I also had good help from this project: https://koumoul-dev.github.io/vuetify-jsonschema-form/latest/

Basically, to add a function dynamically, just implement the render() function instead of using a template. Works a bit like React. You can implement any logic in here to choose the tag, the options, everything. Just return that, and Vue will build the shadow-DOM and keep the real DOM up to date.

The methods in here seems to manipulate the DOM directly, which I'm glad I no longer have to do.

Related