Vue-next: Feature Request: Create app with same appContext? createChildApp

Created on 11 Sep 2020  ·  14Comments  ·  Source: vuejs/vue-next

What problem does this feature solve?

Somethings I need to created detached components.

For example I may call this.$Message.info(content) in which content may be a render function and the component created will be mounted on document.body.

For example:
Before calling $Message.info

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

After calling $Message.info

<body>
  <div id="app">
  <div class="message" />
</body>

Calling createApp(MessageComponent).mount(document.body) inside $Message.info may render the component in body. However the render function will use the new appContext rather than the original appContext which has already been registered with custom components. For example:

this.$Message.info(() => h('component-registered-on-app', ...))

What does the proposed API look like?

const app = createApp(rootComponent)
app.mount(document.body)
const childApp = app.createChildApp(detachedComponentWhichNeedSameContext)
childApp.mount(document.body)

Most helpful comment

It's not documented but yes it's possible to render using existing app context:

import { h, render } from 'vue'

const childTree = h('div', 'some message')
treeToRender.appContext = app._context
render(childTree, document.getElementById('target'))

We can make this a method on the app instance:

app.render(h('div', 'some message'), document.getElementById('target'))

All 14 comments

Maybe you should use resolveComponent or Teleport?

import { resolveComponent } from 'vue'

this.$Message.info(() => h(resolveComponent('component-registered-on-app'), ...)) // Error ❌

// Edited
const messageContent = resolveComponent('component-registered-on-app')
this.$Message.info(() => h(messageContent, ...))

Maybe you should use resolveComponent ~or Teleport~?

import { resolveComponent } from 'vue'

this.$Message.info(() => h(resolveComponent('component-registered-on-app'), ...))

resolveComponent works if it is available in the current application instance. However it's called in new app.

Teleport won't work. Message won't be rendered in the current component tree. What I called message is a toast in material design.

Let me explain more percisely.

pseudo code in vue 2/3

index

import otherComponents from 'ui'
import MessagePlugin from 'message'
import Vue from 'vue'

Vue.use(otherComponents)
Vue.use(MessagePlugin)

Vue3
import otherComponents from 'ui'
import MessagePlugin from 'message'
import { createApp } from 'vue'

const app = createApp(root)

app.use(otherComponents)
app.use(MessagePlugin)

MessagePlugin

Vue.prototype.$Message = {
  info (content) {
    document.body.appendChild((new Vue(MessageComponent, {
      propsData: { content }
    })).$mount())
  }
}
Vue3
app.config.globalProperties.$Message = {
  info (content) {
    createApp(MessageComponent, {
      content
    }).mount(document.body)
  }
}

use it

...
Vue2
  this.$Message.info(h => h('other-component'))
Vue3
  this.$Message.info(() => h(???)) // 'other-component' won't be resolved since it's in another app
...

However in vue-next. (new Vue(root)).$mount() is replace by createApp(root).mount(). The original MessageComponent's content render function will resolve components from Vue so different root can share the same installed components. However the new Message component will resolve components from app which means the originally installed components won't be resolved in new Message component.

This is one way to implement the same features in vue3. No need to use a new app.
Sorry for using jsx

// Message HOC
import { Teleport, defineComponent, provide, ref } from 'vue'

const Message = ({ open, text }) => (
  <Teleport to="body">{open && <div className="modal">{text}</div>}</Teleport>
)

// HOC
const withMessage = (Comp) =>
  defineComponent({
    setup() {
      const show = ref(false)
      const text = ref('Message')
      provide('message', (t) => {
        show.value = true
        text.value = t
      })

      return () => (
        <>
          <Comp></Comp>
          <Message open={show.value} text={text.value}></Message>
        </>
      )
    },
  })

use it

import { createApp, defineComponent, inject } from 'vue'

const App = defineComponent({
  setup() {
    // Use
    const useMessage = inject('message')
    const onClick = () => useMessage(<div>new message</div>)

    return () => <button onClick={onClick}>show message</button>
  },
})

createApp(withMessage(App)).mount('#app')

This is one way to implement the same features in vue3. No need to use a new app.
~Sorry for using jsx~

// Message HOC
import { Teleport, defineComponent, provide, ref } from 'vue'

const Message = ({ open, text }) => (
  <Teleport to="body">{open && <div className="modal">{text}</div>}</Teleport>
)

// HOC
const withMessage = (Comp) =>
  defineComponent({
    setup() {
      const show = ref(false)
      const text = ref('Message')
      provide('message', (t) => {
        show.value = true
        text.value = t
      })

      return () => (
        <>
          <Comp></Comp>
          <Message open={show.value} text={text.value}></Message>
        </>
      )
    },
  })

use it

import { createApp, defineComponent, inject } from 'vue'

const App = defineComponent({
  setup() {
    // Use
    const useMessage = inject('message')
    const onClick = () => useMessage(<div>new message</div>)

    return () => <button onClick={onClick}>show message</button>
  },
})

createApp(withMessage(App)).mount('#app')

It's reasonable.

However it seems the API app.use(MessagePlugin) won't work in the case. Is there any possibility to keep the API?

createApp(withMessage(withNotification(withConfirm(app))) is an ugly api composition.

This is one way to implement the same features in vue3. No need to use a new app.
~Sorry for using jsx~

use it

What's more, when using sfc the usage is too complicate for a component library user.
despite of message-privider, new usage

export default {
  setup () {
    return {
      message: inject('message')
    }
  },
  methods: {
    do () {
      this.message.info()
    }
  }
}

original

export default {
 methods: {
    do () {
      this.message.info()
    }
  }
}

If you want your library to render elements, give the user a component to put in their app where you pass the messages. The solution with a hoc by @lawvs also works


Remember to use the forum or the Discord chat to ask questions!

I think this deserves further consideration.

The suggestion in the documentation to 'create a factory function' to share application configuration is much less straightforward than it sounds.

I've seen people ask about this several times on the forum and Stack Overflow, so it seems to be a common problem, and so far I haven't seen a really compelling answer. Perhaps it's just a documentation problem but either way it is a problem that needs addressing properly.

I've meet same issue here when I try to upgrade my vue plugin libriary to vue3. It seems like there is no better way except create a factory function or hoc, but the usage is too complicate for a component library user.

Hope the vue team can porvide some other ways to slove this problem.

If you want your library to render elements, give the user a component to put in their app where you pass the messages. The solution with a hoc by @lawvs also works

Remember to use the forum or the Discord chat to ask questions!

Indeed @lawvs's solution is a good solution, what about writing 3rd party library? When 3rd party library exports Message method for user to use, the user will have to do things like withMessage(app) in order to get the message component injected into the app.
I am working on a library ElementPlus, my implementation here works for now, but the component itself is sandboxed from the main app, which means it cannot fetch any global data from the app itself, this could potentially be a problem when user need to access global data like config.

It's not documented but yes it's possible to render using existing app context:

import { h, render } from 'vue'

const childTree = h('div', 'some message')
treeToRender.appContext = app._context
render(childTree, document.getElementById('target'))

We can make this a method on the app instance:

app.render(h('div', 'some message'), document.getElementById('target'))

I did some experimenting using the idea @yyx990803 suggested. Here is a crude demo:

https://jsfiddle.net/skirtle/94sfdLvm/

I changed the API slightly:

// To add
const vm = app.render(Component, props, el)

// To remove
app.unrender(vm)

The reasoning behind my changes was:

  1. We need a way to remove the newly rendered content.
  2. This API for render is more similar to createApp and mount, albeit combined into one (I've glossed over SSR but I don't see any reason that couldn't be supported).
  3. This keeps explicit VNodes out of it.
  4. It ensures that all VNodes and elements have an associated vm. I imagine this'll be easier for the Devtools to handle if nothing else.
  5. The new vm is returned by render. It isn't clear how it would be available otherwise.

I ran into a problem trying to render multiple things to the same parent, which I think is important for the use cases here. In my demo I bodged around it by adding in an extra <div> for each item but that isn't ideal as it pollutes the DOM with extra junk.

    import { createVNode ,render} from 'vue'
const body = document.body;
const root = document.createElement("div");
body.appendChild(root);
root.className = "custom-root";
export default {
    install(app){
        let div = document.createElement("div");
        root.appendChild(div);
        // youCom 为自己写的组件,  SoltChild 可以是自己的子组件 ,也可以不传
        let vm = createVNode(youCom,{},{
            // slots
            default:()=>createVNode(SoltChild)
        });
        vm.appContext = app._context; // 这句很关键,关联起了上下文
        render(vm,div);

    }
}

I developed a small tool that allows me to use functions to mount VueComponent.
:biking_man: vue-create-component

Was this page helpful?
0 / 5 - 0 ratings