Querying this.array.length in ref method causes recursive loop
Version
3.0.11
Reproduction link
Steps to reproduce
Self evident, causes "Uncaught (in promise) Error: Maximum recursive updates exceeded". Comment out or remove the 'console.log' line fixes the issue.
What is expected?
No error
What is actually happening?
Array length increases until maximum recursion limit, throwing 'maximum recursive update exceeded' exception.
This hasn't anything to do with arrays in particular. The issue is that the template ref function is run inside the render phase, during which you should not have any side-effects that , as that would trigger a re-render.
The same problem would, for example, manifest if you do this:
<template>
<h1> {{ msg = msg + '!' }}</h1>
</template>
<script>
export default {
data: () => ({
msg: 'Hello World'
})
}
</script>
Coming back to your issue, you might think that we then should just pause reactivity tracking while running a ref function. but then, you could not i.e. have a watch
on that array (which might be a legitimate use case in a composable using that array for something), because adding elements to it with the template ref function would not trigger any reactive effect.
Another way to deal with this is to mark the array to be non-reactive (markRaw()
), and react to its change in the update
lifecycle instead.
So I would say that this is not a bug, and rather something we should document better.
Thanks @LinusBorg and please excuse any ignorance on my part. Here's what's confusing me:
- Everything is fine until I add the
console.log
into the ref function. Without it everything works fine. - If I build the same demo using the Composition API, with the console.log, everything works fine too.
I gave the example using the options API because we're heavily invested in that for our app, and refactoring everything into the composition API would be a lot of work. But for the demo I gave above I can show that it works using the composition API without throwing the recursive error.
In your example it's clear what's causing the recursive render, but my biggest question is why would/should a console.log force a reactive change in the above scenario? That's why it felt like a bug to me.
Thanks! 🙂
Without the console.log, your template has no dependency on arr
. So changing arr
doesn't cause a re-render.
But when you do the console log ( which happens during the render phase), you access the arr.length, which means, the length of the array is now a dependency of your render function, and changing the arrays length will therefore cause a re-render.
In short, the console.log in the template ref function has the same effect as if you did:
<div :ref="(el) => arr.push(el) ">
{{ arr.length }}
</div>
... you are both mutating a reactive array and read from it in the template, so to speak, leading to an endless re-render loop.
If you want feedback on the composition variation of your example, please share it.
For this use case:
<div :ref="(el) => arr.push(el) ">
{{ arr.length }}
</div>
Because the above code does not work, I'm thinking if users just want to do this, then what is the correct way 🤔
<div :ref="setItemRef" v-for="i in 3" :key="i"></div>
const itemRefs = reactive<any>([])
const setItemRef = (el: any) => {
if (el) {
itemRefs.push(el)
}
console.log(itemRefs.length)
}
return {
setItemRef
}
this also not work