'How to add Vue 3 components programmatically?

Vue 3 doesn't have Vue.extend() method, so example here doesn't work: https://css-tricks.com/creating-vue-js-component-instances-programmatically/

I have tried this solution: https://jsfiddle.net/jamesbrndwgn/fsoe7cuy/

But it causes warning in console:

Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref.

enter image description here

So, what is the proper way to add components dynamically (programmatically) in Vue 3?

UploadTaskTagManager.vue

<template>
    <button @click="addTag()" type="button">Add new tag</button>
    <br>
    <div>
        <div v-for="child in children" :key="child.name">
            <component :is="child"></component>
        </div>
    </div>
</template>

<script>
    import UploadTaskTag from "./UploadTaskTag";

    export default {
        name: "UploadTaskTagManager",
        components: {UploadTaskTag},

        data() {
            return {
                children: []
            }
        },

        methods: {
            addTag() {
                this.children.push(UploadTaskTag);
            },
        }
    }
</script>

UploadTaskTag.vue

<template>
    <div>
        <select @change="onChangeTag($event)" v-model="currentTag">
            <option v-for="tag in getAllTags()" :key="tag.tag_name" :value="tag">{{tag.tag_name}}</option>
        </select>
        <input maxlength="16" class="tag-input" ref="inputField"/>
        <button @click="removeTag($event)" type="button">-</button>
        <br>
    </div>
</template>

<script>
    export default {
        name: "UploadTaskTag",

        data() {
            return {
                tags: [],
                currentTag: {}
            }
        },

        methods: {
            onChangeTag(event) {
                this.$refs.inputField.value = this.currentTag.tag_name;
            },

            getAllTags() {
                return this.$store.state.allTags;
            },

            removeTag() {
                this.$el.parentElement.removeChild(this.$el)
            },

            getValue(fieldIndex) {
                let tag = this.tags[fieldIndex];
                return tag ? tag.tag_name : "";
            },
        }
    }
</script>


Solution 1:[1]

just use createApp instead of Vue.extend here's a example of simple vue3 components programmatically

import Button from 'Button.vue'
import { createApp } from "vue"
// Creating the Instance
// use createApp https://v3.vuejs.org/api/global-api.html#createapp
var ComponentApp = createApp(Button)
import Button from "./Button.vue"

// inserting to dom
const wrapper = document.createElement("div")
ComponentApp.mount(wrapper)
document.body.appendChild(wrapper)

set props or slot is a little different by use h, https://v3.vuejs.org/api/global-api.html#h

import Button from 'Button.vue'
import { createApp, h } from "vue"

var ComponentApp = createApp({ 
  setup () {
    return () => h(Button, {type: "primary"}, "default slot as children")
  }
})

Solution 2:[2]

If I understood you correctly, then the solution will be like this

You can learn more about 'defineAsyncComponent' on the official website https://v3.vuejs.org/api/global-api.html#defineasynccomponent

<template>
    <button @click="addTag()" type="button">Add new tag</button>
    <br>
    <div>
        <div v-for="child in children" :key="child.name">
            <component :is="child"></component>
        </div>
    </div>
</template>

<script>

import { defineAsyncComponent, defineComponent, ref } from "vue"

export default defineComponent({
components: {     
      UploadTaskTag: defineAsyncComponent(() => import("./UploadTaskTag"))
    },
 setup() {
      const children = ref([])

      const addTag = () => {
        children.value.push('UploadTaskTag')
        console.log(children.value)
      }
return {
        addTag,
        children        
      }
    },
  })
</script>

Solution 3:[3]

I needed to do such a thing to add a Component to a contenteditable div.

To do so, I used the Teleport component, bound to and array containing the name of the wished componend, the div where it'll be mount and optionaly the props.

Here is a generic code sample.

Image = {
  template: document.getElementById('image-template'),
  props: ['src']
}

const app = Vue.createApp({
  template: document.getElementById('app-template'),
  components: {
    Image
  },
  data() {
    return {
      dynamicComponents: []
    }
  },
  methods: {
    async addImg() {
      let span = document.createElement("span");
      document.getElementById('main').appendChild(span);
      await this.$nextTick();
      this.$appendComponent(
        'Image',
        span, {
          src: 'https://images-na.ssl-images-amazon.com/images/I/81-jLAuCQzL.png'
        }, {
          click: () => {
            alert('Clicked')
          }
        });
    }
  }
});

app.config.globalProperties.$appendComponent = function(name, target, props, listeners = {}) {
  this.$root.dynamicComponents.push({
    name,
    target,
    props,
    listeners
  });
}

app.mount('#app')
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>

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

<template id="app-template">
  <Teleport v-for="{name, target, props, listeners},i in dynamicComponents" :key="i" :to="target">
    <Component :is="name" v-bind="props" v-model="props.modelValue" v-on="listeners"/>
  </Teleport>
  
  <button @click="addImg">Add Image</button>
  
  <div id="main">
    Coucou
  </div>
</template>

<template id="image-template">
  <img :src="src" />
</template>

You can now programatically create DOM nodes anywhere, then adding an entry to the $root.dynamicComponents array will mount a component in it. Context consistant and no bloating.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ethan_you
Solution 2
Solution 3