Subscribe on changes!

reactivity does not work on root component props when using createApp

avatar
Oct 28th 2021

Version

3.2.20

Reproduction link

sfc.vuejs.org/

Steps to reproduce

Pass a reactive({}) object into createApp's second parameter, update a property on it.

What is expected?

The root component should update.

What is actually happening?

The root component is not updating.

avatar
Oct 28th 2021

Here is a non-ideal workaround that requires modifying the props shape of the root component (this may not be ideal if the root component is imported from a third party).

sfc.vuejs.org

avatar
Oct 28th 2021

I would say you need to pass an object of refs to props or an object of reactive values but that seems to warn 🤔 This is probably an edge case with how the props are handled for createApp()

avatar
Oct 28th 2021

I'd rate this as an enhancement. To me, the props passed in always were initial values only.

app.mount() isn't written in a way suitable to listen for a reactive effect from these props either, so it likely is this way by design.

avatar
Oct 28th 2021

So the question is, how does one import any component, mount it as root, and update its props over time? I can't find it in the docs.

This seems like a basic necessity and makes me wonder how it was overlooked. No one ever needs to do this?

avatar
Oct 28th 2021

I can't remember when I last needed that. Though I can remember that I did use a wrapper component for such a thing once.

avatar
Oct 28th 2021

And don't get me wrong, it would make sense to allow/support it I think. It's just not a bug in my book.

avatar
Nov 3rd 2021

It seems like a "regression" in terms of what we could do in Vue 2 but no longer can in Vue 3. This is Vue 2:

import ThirdPartyComponent from 'somewhere'

const component = new Vue({...ThirdPartyComponent})
// ...
component.someProp = 123 // it works.

In Vue 3?

avatar
Nov 3rd 2021

I can't remember when I last needed that.

I seem to need it often.

The main use case is that, when interfacing with other technologies, one often has to mount multiple root components at multiple interface points.

For example, working with data analytics dashboards like Grafana, one has to mount a root component in each dashboard panel as a Grafana plugin.

Or for example, NASA's OpenMCT mission control dashboard allows plugins to add panels with custom visuals. The plugin system passes a plugin a DOM element, and the plugin can then append any DOM that should appear in that area. The plugin author may choose to create the DOM subtree any way they want (plain JS, Vue, React, etc).

Or for example Shopify plugins: same concept: custom DOM trees inserted into online stores to add functionality.

Another example is tech migration: a migrating an app from React (or anything else) to Vue would require data passing at the intersection points where the non-Vue tree turns into a Vue tree.

As another example, without this feature in Vue 3, importing and using 3rd party components as root components (f.e. importing a Vue chart library to stick it in a panel of some dashboard framework) becomes impossible in Vue 3 without having to make a wrapper component just to pass data through (and not even as top level properties to the wrapper component, but as nested second-level reactive properties to the wrapper component as described above), which is more cumbersome than simply setting JS properties on the 3rd-party components.

Etc.


If we're talking about a pure Vue app that is made with nothing but Vue, then this issue disappears. Of course, in this case, the top level component usually has no props.

avatar
Nov 3rd 2021

Just for the record what you can quite easily do for now is use a wrapper component like this:

import { h, reactive } from 'vue'
import App from './App.vue'

const props = reactive({ whatever })

const app = createApp({
  render: () => h(App, props)
})

app.mount('#app')

When you now mutate props, App will update.

Is that what you refer to here?

becomes impossible in Vue 3 without having to make a wrapper component just to pass data through (and not even as top level properties to the wrapper component, but as nested second-level reactive properties to the wrapper component as described above), which is more cumbersome than simply setting JS properties on the 3rd-party components.

I'm not sure I get the "not even as top level ..." part that seems to make it so cumbersome. It's literally a one-line change.

And again, I'm with you that this would be a nice addition, just documenting what does work now.

avatar
Nov 18th 2021

@LinusBorg That's good to know. I never used h before, and I had no idea that passing props to h() would cause props to be reactive. I always used SFC format, and instantiating a root component written in SFC.

How exactly does h() observe changes to the plain object? Is it mutating the object descriptors like Vue 2?

avatar
Nov 18th 2021

Sorry for confusing you - I simply forgot to wrap it in reactive(). Fixed my previous post.

avatar
Nov 19th 2021

Ah, thanks, I will try this out when I circle back to it.

avatar
Nov 20th 2021

You can make it even shorter when using a functional component:

https://stackblitz.com/edit/vue-tv16yo?file=src%2Fmain.js

avatar
Dec 15th 2022

Just for the record what you can quite easily do for now is use a wrapper component like this:

import { h, reactive } from 'vue'
import App from './App.vue'

const props = reactive({ whatever })

const app = createApp({
  render: () => h(App, props)
})

app.mount('#app')

When you now mutate props, App will update.

Is that what you refer to here?

becomes impossible in Vue 3 without having to make a wrapper component just to pass data through (and not even as top level properties to the wrapper component, but as nested second-level reactive properties to the wrapper component as described above), which is more cumbersome than simply setting JS properties on the 3rd-party components.

I'm not sure I get the "not even as top level ..." part that seems to make it so cumbersome. It's literally a one-line change.

And again, I'm with you that this would be a nice addition, just documenting what does work now.

This is nice and seemed to have worked for me for reactivity, but I am using the result of const mounted = app.mount(el) where that is the defineExpose() result.

But when using the render function way such as () => h(component, props), the mounted variable does not return the defineExpose() result as it does when I use createApp(component, props), and I need the result of defineExpose, to expose the interface of my component.

Any idea how to get the defineExpose result?

avatar
Dec 16th 2022

Guys! I figured it out! the full solution to maintain full props reactivity and get the defineExpose() interface! Here is how:

function createComponent ({ app, component, props, el }) {
  
  let expose = null
  
  const childApp = createApp({ render: () => expose = h(component, props) })

  Object.assign(childApp._context, app._context) 

  childApp.mount(el)

  return expose.component.exposed
}

By supplying expose variable into the render function, and then calling childApp.mount(el), the expose variable gets assigned from null to the context, from where you can access expose.component.exposed param to get the exposed interface of your component!

:-)

Now you can use reactive({ props }) as props and they will all be reactive.