vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
47.03k stars 8.24k forks source link

Custom elements sometimes not rendered correctly inside a v-for loop #6309

Open simonvizzini opened 2 years ago

simonvizzini commented 2 years ago

Vue version

3.2.26

Link to minimal reproduction

https://really-sorry-but-no-repro-available.com

Steps to reproduce

Unfortunately we're not able to reproduce this issue in a minimal reproduction example, but I'll try to explain our findings as detailed as possible and maybe some other users who encountered the same issue will be able to provide additional details. I'll also add more details if we encounter any new findings.

We've recently migrated our Vue2 application to Vue3. Our app relies heavily on 3rd party web components and we encountered a major issue related to rendering of custom elements inside v-for loops. Sometimes, but not always, the first custom element is rendered correctly while all subsequent custom elements are rendered in a "broken" state. What's super strange is that this issue seems to be somehow triggered at build time, because some builds are working totally fine while other builds, which may contain only minor, unrelated code changes, suddenly exhibit this problem. We couldn't find a pattern how to trigger this issue, but the issue appears maybe 1 out of 4 times when we build & deploy our application. For local builds in development mode this issue never happened, so it seems only production builds are affected. My uneducated guess is that maybe cosmic rays are affecting the Vue template compiler at build time... :)

We've encountered this issue in two of our templates so far that render custom elements in a v-for loop:

  1. In the first instance we're rendering a simple icon component as custom element, like this: <wb-icon name="checkbox"></wb-icon>. Nothing fancy, no slots, just a name attribute to specify which icon should be rendered. But when the issue happens only the very first icon rendered in the v-for loop (index === 0) will have the name attribute rendered, while all other wb-icon elements have it missing. We were able to workaround this issue by using the new explicit attr modifier of the v-bind directive, like so: <wb-icon :name.attr="'checkbox'"><wb-icon>
  2. The second instance is a bit more complicated and interesting. The web component in question is a web component developed with Stencil without shadow DOM (due to legacy reasons). This component provides a couple of child components, some with default slots. Just to illustrate the template structure, it looks similar to this:
    <div v-for="item in items" :key="item.id" class="item-wrapper">
    <ce-component> <!-- provides default slot -->
     <div class="container">
       <ce-component-child-one> <!-- provides default slot -->
         <div>.....</div>
       </ce-component-child-one>
       <ce-component-child-two /> <!-- standalone component, no slot -->
     </div>
    </ce-component>
    </div>

    Again the first time the elements are rendered in the v-for loop everything is fine, but all subsequent iterations are broken. The DOM elements provided via default slots are rendered correctly, but the DOM elements rendered by the web component itself are completely missing. It seems like Vue's template engine is somehow falsely tampering with the DOM tree rendered by the custom element itself, removing nodes that it is not aware of. And again this issue only happens sometimes for production builds! Super weird and unexpected... We never encountered these kind of issues with Vue 2.

This is a really baffling issue and I'm not sure what other kind of information to provide to help identify the root cause. Please let me know if I can provide any other details. We're still investigating and trying to find a trigger for this issue and will post updates here if we find anything new.

What is expected?

Custom elements rendered in v-for loops are always rendered correctly.

What is actually happening?

Randomly, some production builds will not render custom elements correctly.

System Info

No response

Any additional comments?

No response

einavk commented 2 years ago

Hi, I'm having similar issue - custom elements who repeat themselves - the first one is ok and rest are broken. Also - only happens on global.prod.min and not on dev env. However, in my case, it is not related to v-for at all. I noticed that the "broken" custom elements are my "static" elements, and not those who have some vue binding. for example, I have this one which gets broken:

<my-button
                    class="magic-button"
                    type="secondary"
                    size="small"
                    icon="more-actions"
                ></my-button> 

It is a simple button, with an icon, created using latest lit element and based on a native <button> If I add to the template an identifier, such as:

<my-button
                    class="magic-button"
                    type="secondary"
                    size="small"
                    icon="more-actions"
                    :data-id="`btn_${id}`"
                ></my-button> 

The web component rendered in the browser is not broken anymore. @simonvizzini - have you tried something like that?

micheal-parks commented 2 years ago

This issue is affecting our team as well. For our app (which is using Vue3) some web components are not rendering correctly in very specific circumstances. One circumstance involves rendering web components within v-for loops, another involves un-mounting and re-mounting an element, and others are more unpredictable. In all cases it seems that attributes are no longer passed to the web component, resulting in them defaulting to being rendered in their most default state with no props.

einavk commented 2 years ago

@micheal-parks I agree that it seems the props are not passed to custom element. Does this happen with custom elements that have vue binded props as well?

micheal-parks commented 2 years ago

@einavk We've only noticed this with custom elements with constant string attributes, but this issue is hard to replicate or anticipate a root cause so we're not sure if it affects vue binded props. I'll update this thread if I notice anything else.

This is beginning to affect our app pretty badly, so for now we're exploring interim solutions like shipping vue in development mode in our prod environment, which is less than ideal. To the vue core team: is there any chance of this issue being prioritized in the near future? Knowing that would greatly inform what our options are.

micheal-parks commented 2 years ago

Some additional info: we've discovered that attaching event listeners to custom elements causes this problem to go away. So for now we've added the following hack for each custom element that has this rendering issue:


<custom-element
  @mouseenter="() => { /* no-op */ }"
/>
einavk commented 2 years ago

@micheal-parks this is very similar to what we've done, except we bind some randomly generated string to data-id.

episage commented 2 months ago

Can confirm. It seems to be some race condition in Vue internals. It is deterministic once present however seemingly unrelated changes may cause the bug to resurface.

@micheal-parks Thanks for the workaround.

episage commented 2 months ago

Apologies but the @micheal-parks workaround doesn't work either. The only reliable method is to trigger rerender after mounted.

<script setup>
// https://github.com/vuejs/core/issues/6309
let props = defineProps({
  id: {
    type: String,
  },
});

let renderMe = ref(false);

onMounted(() => {
  renderMe.value = true;
});

</script>
<template>
  <em-emoji v-if="renderMe" :shortcodes="props.id"></em-emoji>
</template>
einavk commented 2 months ago

@episage, have u happen to try my workaround? It seems to be working fine for my team.

episage commented 2 months ago

@einavk I tried with

:key="`${Math.random()}`" :key-x="`${Math.random()}`" 

but to no avail.

Is this what you had in mind?

einavk commented 2 months ago

Yes, yet we did not bind to key but to a custom data attribute, such as data-id. I'm not sure it matters. We are working with vue version 3.3.7 which might also make some difference.