When v-for and ref are used in a parent component with a slot, ref is called repeatedly.
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>
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
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.
I don't think you should use
refs.length
,but rather uselist.length
.refs.length
andrefs.set
will loop until the maximum number of updates are reached. In the real scenario, thev-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 methodexpose
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
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:
App
renders, also rendering its childLayoutWrap
.- The template refs from
:ref="refs.set"
are called. - This changes
refs.length
. The{{ refs.length }}
in the template triggers a re-render. Because it is in the slot, it is theLayoutWrap
component that re-renders, notApp
. - The
onBeforeUpdate
is onApp
, notLayoutWrap
, so that is not triggered. - Re-rendering
LayoutWrap
causes the:ref="refs.set"
to run again. This adds all the same elements to the array again. - This takes us back to step 3. We keep going round and round until Vue detects the infinite recursion and bails out.
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:
App
renders, also rendering its child .LayoutWrap
- The template refs from are called.
:ref="refs.set"
- 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
- The is on , not , so that is not triggered.
onBeforeUpdate``App``LayoutWrap
- Re-rendering causes the to run again. This adds all the same elements to the array again.
LayoutWrap``:ref="refs.set"
- 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?
@wht300 if you don't use refs.length
, will there still be this problem?Is this refs.length
really necessary to use?
@wht300 if you don't use
refs.length
, will there still be this problem?Is thisrefs.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
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.
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 useconst refs = ref([])
withref="refs"
in the template (and likewise forrefs2
) 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?
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