:ref="someFunc" triggered twice for the same Element
Version
3.2.13
Reproduction link
Steps to reproduce
- Setup the vue template with a HTML Element
- add :ref="dom" to that HTML Element
- create the composition API
dom(ref)
for :ref="dom" : it is to show messages for the dom when it is assigned to the refinput
- load the page
What is expected?
only 1 triggering of dom(ref)
input.value changes from null to [HTMLInputElement] only
What is actually happening?
after 1st triggering, there is another triggering for the same reference
2nd triggering:
input.value changes from [HTMLInputElement] to [HTMLInputElement] and these two HTML Elements are identical
Same some redundant callings in Vue3 happened for ref
see #4084
This is expected, setRef will execute on each time patch and unmount.
I think your case is okay, since the content or prop is changed.
But to my case here, there is no any change to the DOM. There is only one rendering. ( just mount to the page and that's all )
hm ok, that's weird then.. if you switch to script-setup it works as you intended demo. Anyway, whether it's bug or not it's easy to fix just use onMounted
hook.
The issue is that the assignment of the ref is done during rendering, where we still collect reactive dependencies of the render/template.
In the provided example, you read input.value
, which makes it a reactive dependency of the template.
Then you change input.value
, which causes a re-render, as a reactive dependency of the template changed.
To see this in action, just comment out the assgnment:
let was // = input.value
Now it will run only once.
As @zhangenming showed, we could work around that by pausing tracking during the ref assignment, but as of now I'm not sure if that is a problem for other use cases and/or should be considered a breaking change.
If the programmers do not know :ref="someFunc" is called for any change including the value of ref
, it would be a disaster of the whole application.
Recursive Calling due to this issue
(I think 102 is just because the playground stopped the calling)
That's expected behavior because the initial ref is empty before render, and is changed on first render when the element is created. Your ref function accesses the value which registers it as a dependency of your render process, so the render will run again because you mutated a dependency right afterwards. The dependency collection should not be disabled inside ref functions because it's possible you want to dynamically determine which ref to put the value into.
Your hypothetical case is unlikely to happen in real world cases. Similar infinite loops have always been possible by doing the same thing inside render functions or watchers.
I made a solution for this issue.
SFC Playground - non-reactive refDom
refDom object is intentionally not vue-reactive
such that it can avoid the recursive behavior due to variable changing and can be used inside reactive()
too
use onMounted is much proper but it sometimes the developer wants to watch the immediate change in ref
This implementation can use for changing ref inside vue-reactive object too
<template v-for="entry in entries" :key="entry.key">
<div :ref="entry.div.setRef">My Div {{entry.key}}</div>
</template>
const entries = reactive([
{
key: 1,
div: refDom(),
},
{
key: 2,
div: refDom()
}
])
As it is non-vue-reactive, watch(entry, ()=>{...})
would not fire for any dom ref change.