Subscribe on changes!

mismatched child nodes count in `patchBlockChildren` when rendering component returning render function in JSX from `computed()` with `<slot v-if="ref">`

avatar
Feb 11th 2024

Vue version

3.4.18

Link to minimal reproduction

https://stackblitz.com/edit/vitejs-vite-cs8k7r?file=src%2FScopedSlots.vue%2Csrc%2FApp.vue

Steps to reproduce

Click the Toggle flag button.

What is expected?

No exception was thrown.

What is actually happening?

TypeError: Cannot read properties of undefined (reading 'el')
    at patchBlockChildren (chunk-EVMNRHZZ.js?v=f93e5a51:7163:18)
    at processFragment (chunk-EVMNRHZZ.js?v=f93e5a51:7266:9)
    at patch (chunk-EVMNRHZZ.js?v=f93e5a51:6758:9)
    at ReactiveEffect.componentUpdateFn [as fn] (chunk-EVMNRHZZ.js?v=f93e5a51:7542:9)
    at ReactiveEffect.run (chunk-EVMNRHZZ.js?v=f93e5a51:227:19)
    at instance.update (chunk-EVMNRHZZ.js?v=f93e5a51:7586:17)
    at callWithErrorHandling (chunk-EVMNRHZZ.js?v=f93e5a51:1667:32)
    at flushJobs (chunk-EVMNRHZZ.js?v=f93e5a51:1874:9)

System Info

No response

Any additional comments?

By adding a breakpoint just before the exception was thrown: https://github.com/vuejs/core/blob/3f92126a26a544dfa69211cf2977556a2706bb2c/packages/runtime-core/src/renderer.ts#L968, we can see the length of two children params is mismatched, oldChildren.length will always be less one than the newChildren.length: image

The <FontAwesomeIcon> is returning a render function h() within computed() to dynamically build elements by the component props: https://github.com/FortAwesome/vue-fontawesome/blob/560b07aade4b8b1f909b448391968935ff9a759c/src/components/FontAwesomeIcon.js#L175 https://github.com/FortAwesome/vue-fontawesome/blob/560b07aade4b8b1f909b448391968935ff9a759c/index.js#L296 https://github.com/FortAwesome/vue-fontawesome/blob/560b07aade4b8b1f909b448391968935ff9a759c/index.js#L329-L337

The following approaches will prevent this exception:

  • Remove v-if="!flag" from <slot>
- <slot v-if="!flag" :renderer="renderer" />
+ <slot :renderer="renderer" />
- <slot v-if="!flag" :renderer="renderer" />
+ <slot v-if="!flag">
+   <RenderFunction :renderer="renderer" />
+ </slot>
- <ScopedSlots>
-   <template #default="{ renderer }">
-     <span>
-       <RenderFunction :renderer="renderer" />
-     </span>
-   </template>
- </ScopedSlots>
+ <ScopedSlots />
  const flag = ref(true);
- const renderer = computed(() => (
+ const renderer = (
    <>{flag.value === false && <FontAwesomeIcon icon="times" />}</>
- ));
+ );

leads to clicking the button will not re-create VNode from renderer, but if the initial value of flag is true its reactivity will somehow be resumed:

- const flag = ref(true);
+ const flag = ref(false);
  • Replace the component <FontAwesomeIcon> that is based on a render function with any other component using plain <template> or just some HTMLElement
- <>{flag.value === false && <FontAwesomeIcon icon="times" />}</>
+ <>{flag.value === false && <span>{JSON.stringify(flag.value)}</span>}</>
+ <>{flag.value === false && <div></div>}</>
avatar
Feb 12th 2024

duplicated of #9308