Should keyed v-for array refs be ordered?
Version
3.1.2
Reproduction link
Steps to reproduce
- open sandbox
- open console
- click reverse
- see first and second refs log
What is expected?
second log should be:
[
<div>bar</div>,
<div>foo</div>,
]
What is actually happening?
second log still be:
[
<div>foo</div>,
<div>bar</div>,
]
I would say having a function gives you the flexibility of ordering the nodes however you want but I'm not sure if this was intended
I would expect that the ordering of refs received would be stable between renders (and between Vue2 and Vue3). Without it being stable it makes things like "focus the first ref in the list" quite difficult as the ordering will reverse after the initial render.
Here is a simplified repro example that shows that re-renders reverse the order of the refs compared to the actual rendered content. Repro example on SFC Playground
The problem originates from the fact that patchKeyedChildren
iterates in reverse-order
https://github.com/vuejs/vue-next/blob/ba89ca9ecafe86292e3adf751671ed5e9ca6e928/packages/runtime-core/src/renderer.ts#L2049-L2051
This means that any setRef
calls that occur in patch
that rely on the v-for : key
iteration done in this loop will end up being provided to :ref="..."
functions in reverse order on patch-renders compared to initial-renders.
This is actually pretty problematic for us in terms of migrating to Vue3. We have lots of accessibility functionalities that rely on being able to properly traverse our $refs
for keyboard navigation, focus transferring, etc.
Without a stable, in-order sort for these refs.. we're kinda stuck.
You may be able to work around this problem via passing the intended index of the ref to your handleItemRef
and using it to set the ref into the array instead of relying on .push(ref)
.
The follow example highlights how this could be implemented
<template>
<ul>
<li
v-for="(item, idx) of items"
:key="item.id"
:ref="ref => handleItemRef(idx, ref)">
{{item.name}}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
itemRefs: [],
items: [
{ name: 'Item 1', id: 1 },
{ name: 'Item 2', id: 2 },
{ name: 'Item 3', id: 3 },
]
}
},
methods: {
handleItemRef(refIndex, ref) {
if (ref) {
this.itemRefs[refIndex] = ref
}
}
},
beforeUpdate() {
this.itemRefs = []
}
}
</script>
Note, this cannot be done when dealing with the Vue3 Migration Build's support of V_FOR_REF as the developer isn't dealing with the :ref="<function>"
api yet.
As such, this ordering of refs for migration users will continue to be invalid.
You can actually sort the elements yourself, may be a viable workaround for the time being:
onUpdated(() => {
console.log(sortElementsByPosition(itemRefs));
});
function sortElementsByPosition(elements) {
return elements.slice().sort((a, b) => {
return a.compareDocumentPosition(b) & 2 ? 1 : -1
})
}
though I can't say much about how this would perform for really long lists (couple thousand items)
Interesting. Didn't know about Node.compareDocumentPosition.
As a note, I'd be happy to PR a solution to this issue, however, given the fact that the internals of the patch system are built intentionally with reverse-order patching for optimization I've not touched the code yet as it isn't clear how the project might want to address this.
Any thoughts or guidance here would be appreciated.
You may be able to work around this problem via passing the intended index of the ref to your
handleItemRef
and using it to set the ref into the array instead of relying on.push(ref)
.The follow example highlights how this could be implemented
<template> <ul> <li v-for="(item, idx) of items" :key="item.id" :ref="ref => handleItemRef(idx, ref)"> {{item.name}} </li> </ul> </template> <script> export default { data() { return { itemRefs: [], items: [ { name: 'Item 1', id: 1 }, { name: 'Item 2', id: 2 }, { name: 'Item 3', id: 3 }, ] } }, methods: { handleItemRef(refIndex, ref) { if (ref) { this.itemRefs[refIndex] = ref } } }, beforeUpdate() { this.itemRefs = [] } } </script>
Note, this cannot be done when dealing with the Vue3 Migration Build's support of V_FOR_REF as the developer isn't dealing with the
:ref="<function>"
api yet. As such, this ordering of refs for migration users will continue to be invalid.
@iendeavor @DV8FromTheWorld
Don't rely on the ordering of :ref="someFunc" It is just something uncontrollable. (see #4646)
After reading through #4646 I don't agree to be honest. Your problem was caused by, frankly, not properly handling the reactivity system in Vue.
The same problem would have occurred for any computed / reactive situation, it just might not have been obvious in terms of the result. Given that you aren't setting up erroneous reactive dependencies it will be fine. And if you do need access to do comparisons like that, you can sidestep the reactivity tracking via variable._value
instead of variable.value