Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reactivity does not work on root component props when using createApp #4874

Open
trusktr opened this issue Oct 28, 2021 · 18 comments
Open

reactivity does not work on root component props when using createApp #4874

trusktr opened this issue Oct 28, 2021 · 18 comments
Labels
🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. has workaround A workaround has been found to avoid the problem

Comments

@trusktr
Copy link

trusktr commented Oct 28, 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.

@trusktr
Copy link
Author

trusktr commented Oct 28, 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

@posva posva transferred this issue from vuejs/vue Oct 28, 2021
@posva
Copy link
Member

posva commented Oct 28, 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()

@posva posva added 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. 🐞 bug Something isn't working has workaround A workaround has been found to avoid the problem labels Oct 28, 2021
@LinusBorg
Copy link
Member

LinusBorg commented Oct 28, 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.

@posva posva removed the 🐞 bug Something isn't working label Oct 28, 2021
@trusktr
Copy link
Author

trusktr commented Oct 28, 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?

@LinusBorg
Copy link
Member

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.

@LinusBorg
Copy link
Member

LinusBorg commented Oct 28, 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.

@trusktr
Copy link
Author

trusktr commented Nov 3, 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?

@trusktr
Copy link
Author

trusktr commented Nov 3, 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.

@LinusBorg
Copy link
Member

LinusBorg commented Nov 3, 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.

@trusktr
Copy link
Author

trusktr commented Nov 18, 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?

@LinusBorg
Copy link
Member

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

@trusktr
Copy link
Author

trusktr commented Nov 19, 2021

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

@LinusBorg
Copy link
Member

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

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

wmfgerrit pushed a commit to wikimedia/mediawiki-extensions-WikibaseLexeme that referenced this issue Nov 3, 2022
The vue and vuex versions are pinned to what is currently provided by
MediaWiki core.

This patch needs some non-trivial changes to the jasmine tests:
In order to be able to mutate the inEditMode prop, we need to wrap the
component in a wrapper, because of vuejs/core#4874 not being fixed even
a year later. That in turn prevents us from calling the methods defined
on the component under test. Thus we need two functions to create new
widgets depending on which part of the interface we want to test,
methods or props.

Also, this no longer asserts the internal inEditMode value, because 1)
asserting internals is bad practice anyway, and 2) we no longer have
access to that in the wrapped component.

Bug: T304534
Change-Id: I82313a5eb6e8f19088de4a2e831666cdb656b1eb
Co-Authored-By: Michael Große <[email protected]>
wmfgerrit pushed a commit to wikimedia/mediawiki-extensions that referenced this issue Nov 3, 2022
* Update WikibaseLexeme from branch 'master'
  to 2a16c9e8f4a8e83c379bb88828ed7a18387bd980
  - Fully migrate to Vue 3
    
    The vue and vuex versions are pinned to what is currently provided by
    MediaWiki core.
    
    This patch needs some non-trivial changes to the jasmine tests:
    In order to be able to mutate the inEditMode prop, we need to wrap the
    component in a wrapper, because of vuejs/core#4874 not being fixed even
    a year later. That in turn prevents us from calling the methods defined
    on the component under test. Thus we need two functions to create new
    widgets depending on which part of the interface we want to test,
    methods or props.
    
    Also, this no longer asserts the internal inEditMode value, because 1)
    asserting internals is bad practice anyway, and 2) we no longer have
    access to that in the wrapped component.
    
    Bug: T304534
    Change-Id: I82313a5eb6e8f19088de4a2e831666cdb656b1eb
    Co-Authored-By: Michael Große <[email protected]>
@infinite-system
Copy link

infinite-system commented Dec 15, 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?

@infinite-system
Copy link

infinite-system commented Dec 16, 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.

@vallemar
Copy link

vallemar commented May 31, 2024

@LinusBorg This would be quite useful for custom renderers. In these cases we can create an application in each navigation, modals or any other flow in which you need to inject a component into a new native view. The proposed solutions do not work with ref.

Of course this is not something that is used on the front end, but to make the custom renderer more flexible it should support this.

Additionally, a component can receive props like these:

const refItem = ref([{foo: "bar1"}, {foo: "bar2"}]);
const reactiveItem = reactive({foo: "bar"});
const nonReactiveProp = "foo"

createApp(component, {refItem, reactiveItem, nonReactiveProp})

This is something totally valid in vue components, why not in the main component?

@FANG-NZ
Copy link

FANG-NZ commented Jul 28, 2024

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.

Thanks mate. You saved my life anyway !!!!

@ReinisV
Copy link

ReinisV commented Nov 8, 2024

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.

this should definitely be added to Vue docs, being able to update manually mounted root components props is a core feature of Vue for anyone who renders their apps via different rendering engines (and thats basically anyone who has any kind of legacy, and is unable to serve the prebuilt index.html file).

It shouldnt require an hours worth of googling to find the correct keywords to stumble across this github issue to finally figure out how to achieve this thing, that took 5 minutes to do in Vue2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. has workaround A workaround has been found to avoid the problem
Projects
None yet
Development

No branches or pull requests

7 participants