Subscribe on changes!

Ref conflicts with Global components: Invalid vnode type when creating vnode: .

avatar
Feb 18th 2022

Version

3.2.31

Reproduction link

stackblitz.com

Steps to reproduce

I have created a global component named QRadio as you can see in main.js. In App.vue, I use that component but I am also trying to create a ref to pass a reactive value to that component.

It seems that I can't use qRadio as a ref name because it somehow conflicts with the Global component.

What is expected?

The name of the ref should not conflict with the name of the global component

What is actually happening?

I get an error: Invalid vnode type when creating vnode: .


If I change the name qRadio of the ref then everything works

avatar
Feb 18th 2022

This is working as intended. in script setup, you no longer need to register a component explicitly, rather you can just us a component that you imported - or created in place - by adding an element of the same name as the variable to the template.

And similarly to how "normal" script works, this is expected to override any globally registered components. So, transferred into the "normal" options-based script, you basically did this:

<template>
  <q-radio />
</template>
<script>
export default {
  components: {
    qRadio: ref('')
  }
}
</script>

which we would expect to fail.

Ans yes, I'm aware that you can do the following, but that's not logically possible in script setup - it's either/or as variables are really just values in the render function's (template's) closure.

<template>
  <q-radio />
</template>
<script>
import qRadio from '....'
export default {
  data: () => ({
    qRadio: ref('')
  })
  components: {
    qRadio,
  }
}
</script>

And my standpoint is that the current behavior is by design

avatar
Feb 18th 2022

@LinusBorg thanks for taking the time to look at this.

First, I don't know why you are complicating stuff by putting components option and Options API. Also, your code doesn't make sense. How can you put ref in the components?

export default {
  components: {
    qRadio: ref('')
  }
}

As per the other example, why did you put ref inside the data? Properties get reactive by default there. I have experimented with putting ref and reactive inside data() but I've seen that this is a bad practice. Am I wrong?

export default {
  data: () => ({
    qRadio: ref('')
  })
}

Anyway, I can't believe how the current behavior is made by design. For others who take a look into this, let me show the code I tried:

<script setup>
import { ref } from 'vue';

const qRadio = ref('');
</script>

<template>
  <qRadio />
</template>

From the snippet above, <qRadio /> is a global component. So far so good. Now I am creating a variable qRadio which is a ref with the intention to hold a value. Why is by design that qRadio should interfere with QRadio global component?

To give it some context, in my original code I have a bottomSheet ref variable which tries to be used in a component named BottomSheet: <BottomSheet v-model="bottomSheet" />. I spent a good amount of time trying to figure out what is wrong and I finally found that variable naming interferes with global component naming.

So if you think that's not a bug but it's the intended behavior, I am expecting to get a warning at least so I know what I did wrong.


Probably my personal opinion doesn't count so much but in case it does, here are some bullet points:

  1. Don't you think that an app can have many global components that can clash with ref variables?
  2. Shouldn't this behavior get documented (couldn't find it in the docs)?
  3. Since this is made by design. Don't you think devs won't expect that behavior? I don't know about the others but if this was in an RFC, I would not vote for this behavior because really, it doesn't make sense to me.
  4. Speaking of RFCs is this behavior mentioned in any of them?

Please don't take my arguments against this "intended behavior by design" as negative. I am politely trying to state my thoughts and I respect every decision taken from Vue team. Thank you

avatar
Feb 18th 2022

First, I don't know why you are complicating stuff by putting components option and Options API. Also, your code doesn't make sense. How can you put ref in the components?

I see that I didn't make myself clear, sorry for that. So, let's restart and ignore my previous, confusing examples for a second.

I was trying to demonstrate that

  • in options API, you can override a globally registered component with a locally registered one.
  • in script setup, you don't register components, any top-level variable can be used as a component
  • so in script setup, a variable can override a globally registered component the same way that in options API, a locally registered component can.

Components are defined with variables

Typically, you see this in the common examples:

<script setup>
import {  from 'vue';
import MyComponent from 'vue'
</script>

<template>
  <MyComonent />
</template>

but you technically, can also do this:

<script setup>
import {  from 'vue';
import MyComponent from 'vue'
const qRadio = MyComponent
</script>

<template>
  <qRadio />
</template>

or this:

<script setup>
import { h } from 'vue';

// locally defined functional component just as a quick demo.
const qRadio = () => h('h1', 'I replaced qRadio!')
</script>

<template>
  <qRadio />
</template>

..the öast 2 examples would override the globally registered <qRadio> component. Understand that I'm not saying this is a common thing to do, it's moreso a side effect of how you use components in script setup: any top level variable that matches a component name in the template will be used as the component.

Now, you might think: "I didn't define a component, I declared a ref! Surely Vue must warn me that I'm using something that can't be a component in place of a component!".

Well, it does, at runtime, with the error you are seeing.

Of course it would be better to have a more use case specific error message during compilation, but that would require that the compiler practically evaluates your code, because it would have to i.e. understand the difference between this:

const qRadio = ref('') // this is not a component, so we should warn during compilation, right?

qRadio.value = () => h('div', 'This is a functional component') // oh, now it *is* a component. should we no longer warn?`how do we detect this reliably?

Summary

So, in essence, we can't really warn about this reliably during compilation on a case-by-case basis because we can't infer (with reasonably effort) wether or not a variable defined in script setup actually contains a component.

So, we have two choices, conceptually:

  1. always prefer use a locally defined variable as a component in the template if they match by name, allowing/risking the override of globally registered components (current bahavior.
  2. never allow any locally defined variable to be used as a component when a global component of the same name exists, which would make it impossible to locally replace a component. It would also mean a (likely very minimal) performance impact as we can't optimize for the local variable in the render function's closure, instead we always have to check the global component register as well.

You would likely argue for 2. Which is a fine standpoint to have, but prefering 2. doesn't make 1. an invalid way of working, and thus not a bug. Changing Vue's behavior from 1. to 2. would be a breaking change, which requires a major release.

About your questions

  1. Don't you think that an app can have many global components that can clash with ref variables?

That doesn't reflect my experience, no. But I have no "numbers" on this. However your issue is the first time in my almost daily interaction with users that his came up as an issue.

  1. Shouldn't this behavior get documented (couldn't find it in the docs)?

If the docs are unclear on that, we can certainly improve them. I'll take a look later

  1. Since this is made by design. Don't you think devs won't expect that behavior?

See my answer to your first question.

Also, would you say users would expect for this to fail (example from above)?

<script setup>
import { h } from 'vue';

// locally defined functional component just as a quick demo.
// should this fail with an error? or silently be ignored and the global component being used?
const qRadio = () => h('h1', 'I replaced qRadio!')
</script>

<template>
  <qRadio />
</template>
  1. Speaking of RFCs is this behavior mentioned in any of them?

Please understand that I won't search past RFCs to find mentions for you. You can read the RFC here yourself:

https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md

Also keep in mind there were months of discussions on this and multiple iterations

avatar
Feb 18th 2022

@LinusBorg thank you for the detailed explanation.

While I know why technically this fails and your explanation gives me a better understanding, I still think that this is a bug or at least should get improved. Sorry, I might be the only one who thinks that.

If the dev has a component imported locally, I don't think he/she will run into that issue because even JS won't let you declare the same variable twice. But when the component is globally registered, it's easy to fall into that issue.

Saying that it's not technically possible to understand if the variable is a ref or a component is a valid reason and I can accept that. In my limited knowledge of the internal code of Vue I believe that there must be a way to inspect the type of the variable and see if it's a ref but if you say it's not, then you are most probably right.

Since this is a rare issue, as you say, it should not get too much attention and probably get closed, but I believe an improvement should be made to better warn the user about this issue and/or add something to the docs about it. It can really take time to understand what the issue is and I guess I was lucky to understand that it was a naming issue because my component was a global one and didn't need import.

avatar
Feb 18th 2022

its not rare, you just need to be aware, that when you use <script setup> you get all the magic that comes with it.

if you want to use confusing names for components and variables, you can do that just make sure they are not case conflicting when vue try to resolve them.

This will not conflict

const qRadio = ref('');
  <QRadio />

haven't checked the docs, but resolution logic should be something that is documented.

avatar
Feb 18th 2022

if you want to use confusing names for components and variables, you can do that

@lidlanca I am not using confusing names. If you have read my comments, I said that in my original code I have a global component named BottomSheet and I have a variable bottomSheet which is a ref. And that's when I got the error.

After I figured out that the issue was the ref and global component naming, I tried to reproduce the issue in a simple way using another global component. In my project I use Quasar and all components start with q- so used QRadio component with a qRadio ref and I saw that indeed the issue is due to the naming.


Personally, I don't have a problem living with that "limitation"/"behavior" but I thought it would be helpful to open an issue. If anyone runs into this, at least there is an issue about it.

its not rare

@LinusBorg said:

"is the first time in my almost daily interaction with users that this came up as an issue"

so it seems like a rare case this can happen.

avatar
Feb 18th 2022

when I said confusing I meant visually conflicting/similar. for example names that are just slightly different (ex capital letter) which is especially a problem where template have case insensitivity and more hidden logic.

BottomSheet bottomSheet

HelloWorld helloWorld

qButton QButton

related https://github.com/vuejs/core/issues/5285

avatar
Feb 18th 2022

Closing as this is working as intended as it's also the first line in docs: https://vuejs.org/api/sfc-script-setup.html#using-components

Values in the scope of