Vue-next: Programatically create and mount a component instance

Created on 6 Aug 2020  路  7Comments  路  Source: vuejs/vue-next

What problem does this feature solve?

With vue 2.x, the Vue.component() method did return a component constructor that could be used to programatically create an instance of that component:

const MyComponent = Vue.component('my-component', {
  template: '<div>Hello {{name}}</div>',
  props: ['name']
});
const myComponent = new MyComponent({
   propsData: {
      name: 'Thomas'
   }
});
myComponent.$mount('#my-component-mountpoint');

This feature was very useful for cases where you had to control the lifecycle of a component manually (mount, unmount, destroy).

With vue3 this feature seems to be missing, as the app.component() method only returns the app instance itself.

What does the proposed API look like?

const app = Vue.createApp({});
const MyComponent = app.component('my-component', {
  template: '<div>Hello {{name}}</div>',
  props: ['name']
});
const myComponent = new MyComponent({
   propsData: {
      name: 'Thomas'
   }
});
myComponent.$mount('#my-component-mountpoint');

Most helpful comment

I'd really love to see this use-case getting ported over to Vue 3 as it provides so much flexibility.

In that case, it's worth going through the RFC process. I think this belongs in the user land but maybe there is a lot of people using it and it worth adding a utility to Vue that can be tree shaken

All 7 comments

You have to pass the props to createApp:

Vue.createApp({
  template: '<div>Hello {{name}}</div>',
  props: ['name']
}, { name: 'Thomas' }).mount('#my-component-mountpoint')

@posva I'm afraid your solution doesn't offer the same functionality.

In your example you are creating a new app instance - but I want to create a new component instance of an existing app instance, so other registered app components and mixins can be used by the component instance that is created programatically.

There is no global state anymore, see https://github.com/vuejs/rfcs/blob/master/active-rfcs/0009-global-api-change.md#global-api-mapping

But you can could still create custom createExtendableApp as a superset of createApp that would keep track or registered assets and that exposes an extend function that would create some kind of copy

@posva Thanks for the response. I'm not sure we are talking about the same thing :) To illustrate what I mean I've created a small working example that overwrites the app.component function, so it provides a class that can be used to create, mount and destroy an instance of that component programatically (very similar to how it worked in vue 2). Note that this has nothing to do with global state:

Codesandbox Example:
https://codesandbox.io/s/quizzical-platform-blkxp?file=/index.html

<!DOCTYPE html>
<html>

<head>
  <script type="module">
    import {
      createApp,
      createVNode,
      render
    } from "https://unpkg.com/[email protected]/dist/vue.esm-browser.js";

    const app = createApp({});

    // holds registered component constructor classes
    const _componentConstructors = {};

    // get hold of the original app.component method, it will be overwritten
    const _registerComponent = app.component;

    // monkey-patch app.component to return a constructor class
    // note: overwriting app.component has side-effects, e.g.
    // chaining won't work anymore: app.component('a', ...).component('b', ...).directive('c', ...)
    app.component = (name, component) => {
      // return already registered component instance if no component is passed
      // in order to support: const MyComponent = app.component('my-component')
      if (!component) return _componentConstructors[name];

      // register the component with the vue app
      _registerComponent(name, component);

      // create a class that can be used to create an instance
      // of the component programatically
      class VueComponent {
        /**
         * @param {{ [propName]: value }} [propsData]
         * Data that will be used to populate props
         */
        constructor(propsData) {
          this.name = name;
          this.template = component;
          this.container = null;
          this.mounted = false;
          this.vnode = createVNode(component, propsData);
          this.vnode.appContext = app._context;
        }

        /**
         * Gets the actual component instance proxy (only if mounted)
         */
        get component() {
          return this.vnode?.component?.proxy;
        }

        /**
         * Mounts the component to a container
         *
         * @param {string | Element} container
         * A css selector or Element that we will mount the component to
         */
        mount(container) {
          if (this.mounted)
            throw Error(`Component "${this.name}" is already mounted`);
          if (typeof container === "string")
            container = document.querySelector(container);
          if (!container instanceof Element)
            throw Error("Must pass a valid selector or HTMLElement");
          render(this.vnode, container);
          this.container = container;
          this.mounted = true;
        }

        /**
         * Destroys the component - this will also unmount it
         */
        destroy() {
          if (this.mounted) render(null, this.container);
          this.container = null;
          this.vnode = null;
          this.mounted = false;
        }
      }

      // remember the component constructor class so we can provide it later on
      _componentConstructors[name] = VueComponent;
      return VueComponent;
    };

    ///////////////// TEST ////////////////
    app.component("test-a", {
      template: '<span style="background: green">test a</span>',
      unmounted() {
        console.log("test-a unmounted");
      }
    });
    app.component("test-b", {
      template:
        '<span style="background: orange"><test-a></test-a> & test b</span>',
      unmounted() {
        console.log("test-b unmounted");
      }
    });
    const MyComponent = app.component("my-component", {
      template:
        '<div style="background: yellow"><test-b></test-b> Hello {{name}}, will destroy in {{counter}}s</div>',
      data: () => ({ counter: 10 }),
      props: ["name"],
      mounted() {
        this.intervalId = setInterval(() => this.counter--, 1000);
      },
      unmounted() {
        console.log("my-component unmounted");
        clearInterval(this.intervalId);
      }
    });
    const myComponent = new MyComponent({ name: "Vue" });
    myComponent.mount("#test");
    setTimeout(() => myComponent.destroy(), 10 * 1000);
  </script>
</head>

<body>
  <div id="test"></div>
</body>

</html>

I'd really love to see this use-case getting ported over to Vue 3 as it provides so much flexibility.

I'd really love to see this use-case getting ported over to Vue 3 as it provides so much flexibility.

In that case, it's worth going through the RFC process. I think this belongs in the user land but maybe there is a lot of people using it and it worth adding a utility to Vue that can be tree shaken

I think this belongs in the user land but maybe there is a lot of people using it and it worth adding a utility to Vue that can be tree shaken

Creating a tree-shakable utility for this sounds like a good idea. I don't currently have the time to drive the RFC process for this though. Anyway, I think the sample code above might be a good starting point for people looking for a similar functionality in the meantime. Thanks for your valuable feedback @posva.

I made a module based on the discussion here for anyone who should find this in the future

Was this page helpful?
0 / 5 - 0 ratings