Subscribe on changes!

When v-for and ref are used in a parent component with a slot, ref is called repeatedly.

avatar
Apr 24th 2023

Vue version

3.2.47

Link to minimal reproduction

https://github.com/wht300/vue-error

Steps to reproduce

1、Create a component with a slot

<!-- LayoutWrap.vue --> 
<template>
    <div>
        <slot></slot>
    </div>
</template>

2、Do v-for :ref="refs.set" in LayoutWrap

<!-- App.vue --> 
<template>
  <LayoutWrap>
    on slot
    <template v-for="value in list" :key="value">
      <div v-show="index===value"  :ref="refs.set">{{ value }}</div>
    </template>
    <div>refs.length:{{ refs.length }}</div>
  </LayoutWrap>
  <br>
  <div>
    on div
    <template v-for="value in list" :key="value">
      <div v-show="index===value"  :ref="refs2.set">{{ value }}</div>
    </template>
    <div>refs2.length:{{ refs2.length }}</div>
  </div>
</template>

<script setup>
import {  ref } from 'vue';
import { useTemplateRefsList } from '@vueuse/core';
import LayoutWrap from '@/components/LayoutWrap.vue';

let index = ref(1);
const list=ref([1,2,3])
const refs = useTemplateRefsList();
const refs2 = useTemplateRefsList();
</script>

image

What is expected?

I think this is a Vue bug, the ref function should only be called once in the above case, refs.length should be equal to 3

What is actually happening?

refs.length // 303

System Info

No response

Any additional comments?

No response

avatar
Apr 24th 2023

I don't think you should use refs.length,but rather use list.length.refs.length and refs.set will loop until the maximum number of updates are reached.

avatar
Apr 25th 2023

I don't think you should use refs.length,but rather use list.length.refs.length and refs.set will loop until the maximum number of updates are reached. In the real scenario, the v-for is a custom component. For example, if you need to iterate through the refs when you submit, and you need to loop through the refs when you call the method expose out of the custom component, then there will be a problem like

<!-- App.vue --> 
<template>
  <LayoutWrap>
    on slot
    <template v-for="value in list" :key="value">
      <FormItem :data="value"  :ref="refs.set">{{ value }}</FormItem>
    </template>
    <div>refs.length:{{ refs.length }}</div>
    <button @click="handleSubmit">submit</button>
  </LayoutWrap>
  <br>
</template>

<script setup>
import {ref, unref} from 'vue';
import { useTemplateRefsList } from '@vueuse/core';
import LayoutWrap from '@/components/LayoutWrap.vue';

const list=ref(['formData1','formData2','formData3'])
const refs = useTemplateRefsList();
const handleSubmit=()=>{
  let count=0
  unref(refs).forEach(ref=>{
    ref.validate()
    count++
  })
  console.log(count) // 306
// do somethings
}
</script>
<!-- FormItem.vue --> 
<script setup>
defineExpose({
  validate(){}
})
</script>

When I click submit console.log(count) is 306

avatar
Apr 25th 2023

I believe this is a problem with useTemplateRefsList.

https://github.com/vueuse/vueuse/blob/main/packages/core/useTemplateRefsList/index.ts

useTemplateRefsList is relying on onBeforeUpdate to reset the array, but that won't be called when the template ref is used inside a slot.

Using onBeforeUpdate to reset the array was a common approach for using template refs with v-for, but it couldn't work correctly inside a slot, so Vue 3.2.25 introduced a built-in solution: https://vuejs.org/guide/essentials/template-refs.html#refs-inside-v-for.

To explain in more detail what's happening:

  1. App renders, also rendering its child LayoutWrap.
  2. The template refs from :ref="refs.set" are called.
  3. This changes refs.length. The {{ refs.length }} in the template triggers a re-render. Because it is in the slot, it is the LayoutWrap component that re-renders, not App.
  4. The onBeforeUpdate is on App, not LayoutWrap, so that is not triggered.
  5. Re-rendering LayoutWrap causes the :ref="refs.set" to run again. This adds all the same elements to the array again.
  6. This takes us back to step 3. We keep going round and round until Vue detects the infinite recursion and bails out.
avatar
Apr 25th 2023

I believe this is a problem with .useTemplateRefsList

https://github.com/vueuse/vueuse/blob/main/packages/core/useTemplateRefsList/index.ts

useTemplateRefsList is relying on to reset the array, but that won't be called when the template ref is used inside a slot.onBeforeUpdate

Using to reset the array was a common approach for using template refs with , but it couldn't work correctly inside a slot, so Vue 3.2.25 introduced a built-in solution: https://vuejs.org/guide/essentials/template-refs.html#refs-inside-v-for.`onBeforeUpdate``v-for`

To explain in more detail what's happening:

  1. App renders, also rendering its child .LayoutWrap
  2. The template refs from are called.:ref="refs.set"
  3. This changes . The in the template triggers a re-render. Because it is in the slot, it is the component that re-renders, not .refs.length``{{ refs.length }}``LayoutWrap``App
  4. The is on , not , so that is not triggered.onBeforeUpdate``App``LayoutWrap
  5. Re-rendering causes the to run again. This adds all the same elements to the array again.LayoutWrap``:ref="refs.set"
  6. This takes us back to step 3. We keep going round and round until Vue detects the infinite recursion and bails out.

I agree with you that useTemplateRefsList needs to take this situation into account, but at the same time, there is a problem with the call mechanism of refs.set in :ref="refs.set" on the vue side when it is inside a slot and inside an HTMLElement? Or does it need to be explained in the documentation?

avatar
Apr 25th 2023

@wht300 if you don't use refs.length, will there still be this problem?Is this refs.length really necessary to use?

avatar
Apr 25th 2023

@wht300 if you don't use refs.length, will there still be this problem?Is this refs.length really necessary to use?

I think so. In some business scenarios when I call the expose method of a v-for-generated component via refs traversal, the

// Pseudocode, refs are generated from list v-for
const list=[1,2,3,4]
const errorNums=refs.value.map(item=>item.xxx()).filite(Boolean)
const isAllReady =list.length===errorNums.length

If refs.length has an exception, it may cause errorNums.length>list.length when refs.value.map operates, causing an unexpected bug

avatar
Apr 25th 2023

I don't see a Vue bug here. Using :ref with a function, the function will be called every time it re-renders. The function needs to take account of the case where the passed element or component hasn't changed. useTemplateRefsList isn't doing that.

The changes in 3.2.25 make useTemplateRefsList redundant anyway. If you use const refs = ref([]) with ref="refs" in the template (and likewise for refs2) it should work fine.

avatar
Apr 25th 2023

I don't see a Vue bug here. Using :ref with a function, the function will be called every time it re-renders. The function needs to take account of the case where the passed element or component hasn't changed. useTemplateRefsList isn't doing that.

The changes in 3.2.25 make useTemplateRefsList redundant anyway. If you use const refs = ref([]) with ref="refs" in the template (and likewise for refs2) it should work fine.

That is, if, according to the documentation, when the component update trigger :ref="setFun", if setFun is to maintain the component in the Array, then in the setFun function in addition to determine whether there is el, but also need to determine whether el already exists. Am I understanding this correctly? image

avatar
Apr 25th 2023

After reviewing the code, I now think it should be a lifecycleupdate issue, so I've re-raised an issue look https://github.com/vuejs/core/issues/8153