Skip to content

Instantly share code, notes, and snippets.

@AlexVipond
Last active October 19, 2025 19:48
Show Gist options
  • Save AlexVipond/d0f82933f3451c9b1ed021a942817eb5 to your computer and use it in GitHub Desktop.
Save AlexVipond/d0f82933f3451c9b1ed021a942817eb5 to your computer and use it in GitHub Desktop.
Updating arrays of elements with function refs in Vue 3

Updating arrays of elements with function refs in Vue 3

I did a screencast series recently talking about my favorite Vue 3 feature: function refs. Check out the screencast series for more context-in this gist, I'm going to answer a fantastic question I got in one of the videos' comment section.

This question was about how to properly update arrays of elements that are captured by function refs. Let's check out the basic principle first, before we look at the details of the question.

<script setup>
import { ref, onBeforeUpdate } from 'vue'

// To get references to the elements rendered by `v-for`, we
// first set up an array:
const elements = ref([])

// Then, we write a function ref, which will capture each element
// and store it in the correct order in the array.
const functionRef = (el, index) => {
  // We specifically assign to the `index` property here. We can't assign
  // a new value to the array, otherwise every single element will trigger a new 
  // component update cycle, and we'll have an endless rendering loop.
  //
  // In theory, we could use `.push(el)` here, but in my experience,
  // `push` doesn't preserve the exact order of elements as well as
  // setting the precise index.
  elements.value[index] = el
}

// Before every component update (e.g. updates triggered by changes
// to any other reactive reference in the component), we have to empty
// out the array, to make sure we get rid of any stale references to 
// elements that might not be on the page anymore, or might have
// changed order during re-rendering.
onBeforeUpdate(() => {
  elements.value = []
})
</script>

<template>
  <ul>
    <!-- We bind our function ref to the `ref` attribute
    of the `v-for` element -->
    <li
      v-for="(item, index) in [0, 1, 2]"
      :ref="el => functionRef(el, index)"
    >
      {{ item }}
    </li>
  </ul>
</template>

The important snippet of that code is the onBeforeUpdate hook, which is what the Vue docs recommends you write.

onBeforeUpdate(() => {
  elements.value = []
})

This code does two notable things:

  • It empties out the array of elements, which the update cycle will then refill when the v-for list re-renders.
  • It's an assignment to a reactive reference, so it will trigger any watchers that are watching the elements reference.

The question I got in the comments was:

What is the behavior if you used elements.value.length = 0 in the onBeforeUpdate()

What happens if, instead of running elements.value = [] inside onBeforeUpdate, we run elements.value.length = 0?

Mutating the length property of an array does successfully empty out the array, and it would get rid of any stale element references in our case.

However, mutating length does not trigger reactive updates in Vue.

I made an SFC playground to prove it.

In the playground, there are two arrays: one is "normal", meaning that we clear it out by assigning a new, empty array. The other is "mutated length", meaning that we clear it out by mutating its length property.

As you can see, the watcher that watches the normal array fires on every update, while the watcher that watches the mutated length array never fires.

If you're confident that the list you're rendering with v-for will have the exact same order and exact same length on every render, this is not a problem. Event listeners and attribute bindings won't go stale. It's safe to reset the array on every update by setting its length to 0. But in fact, mutating the length is not even necessary in that case—you can just bind your function ref, and forget about the onBeforeUpdate hook entirely.

// If you're confident that the order and length of `elements`
// won't change, skip the `onBeforeUpdate` hook entirely.
// 
// This code is all you would need:
const elements = ref([])
const functionRef = (el, index) => {
  elements.value[index] = el
}

But if there's any chance the items in your list will change order, or if items will be added or removed, I recommend assigning the empty array instead. Then, you can set up watchers that will detect the element array updates, and inside those watchers, you can check if the array has changed length or order. If so, you'll need to remove stale event listeners and add fresh ones, bind fresh values to attributes, etc.

Here's some sample code for checking whether or not elements' have changed order, or whether elements have been added or removed:

import { ref, onBeforeUpdate, watch } from 'vue'

const elements = ref([])
const elementsRef = (el, index) => elements.value[index] = el
onBeforeUpdate(() => elements.value = [])

// This watcher will run on every update
watch(
  elements,
  (currentElements, previousElements) => {
    if (currentElements.length !== previousElements.length) {
      // The number of elements has changed. Stale side effects
      // need be cleaned up, and fresh side effects need to run.
      return
    }

    // We've established that the number of elements is the same,
    // but it's still possible that the elements could have changed
    // order.
    for (let i = 0; i < currentElements.length; i++) {
      if (currentElements[i].isSameNode(previousElements[i])) {
        // The elements have changed order. Stale side effects
        // need be cleaned up, and fresh side effects need to run.
        return
      }
    }
  }
)
@byronferguson
Copy link

That's an incredible through response. Thank you.

@rtcpw
Copy link

rtcpw commented Oct 10, 2025

The function ref's parameter el can be null, but it seems this case is not handled by the code snippet. How would you go about dealing with it?

@AlexVipond
Copy link
Author

AlexVipond commented Oct 13, 2025

So far, I haven't found a use for storing the null value anywhere, so I usually just early return on falsey values:

function elementRef(el, index) {
  if (!el) return
  elements.value[index] = el
}

Here's the snippet from my more production-grade implementation:
https://github.com/baleada/vue-features/blob/0ec258e64f449154975b26a0b5e549662d07ec8f/src/extracted/useListApi.ts#L89-L93

@rtcpw
Copy link

rtcpw commented Oct 14, 2025

Btw there is one situation that may catch people off guard. Consider the following code:

<template>
  <div v-for="(item, index) in someArray">
    <div v-if="item.condition" :ref="el => functionRef(el, index)">
      <!-- ..... -->
    </div>
  </div>
</template>

In this case, some ref array entries may become undefined, because the functionRef never executes for the items where condition evaluates to a falsy value. So, the elements ref array may contain undefined items within it. Just something to keep in mind.

So, when we have to consider that items in elements may be undefined anyway, I think it is safe to just use the following elementRef implementation:

function elementRef(el, index) {
  elements.value[index] = el ?? undefined;
}

@AlexVipond
Copy link
Author

For that case, I'd rewrite the template to filter the array instead of using nested v-if:

<template>
  <div v-for="(item, index) in someArray.filter(({ condition }) => condition)">
    <div :ref="el => functionRef(el, index)">
      <!-- ..... -->
    </div>
  </div>
</template>

This way, we guarantee that we'll never have undefined or null in the elements.value array.

If there's any possibility that elements.value[someIndex] can be undefined, then we have to introduce a lot of optional chaining and other syntax & logic to all the rest of our code. It's easy to forget that detail and introduce bugs, which makes it ultimately less safe to do elements.value[index] = el ?? undefined.

@rtcpw
Copy link

rtcpw commented Oct 19, 2025

So, I have discovered a major limitation when using the above gist. When you have a code like this:

<template>
    <NestedComponent>
        <div v-for="(item, index) in someArray" :ref="el => functionRef(el, index)">
            <!-- ..... -->
        </div>
    </NestedComponent>
</template>

Then onBeforeUpdate will not get called, and hence the elements will get out of sync. For the above code to work well, you must never use functionRef on an element that is projected through a child component's slot. That's tough to always watch out for.

@AlexVipond
Copy link
Author

Yep, I've hit this limitation on the function-ref-based component library I'm currently working on, and it's a really nasty one to solve. I haven't found any great solutions yet that I would recommend, but will share anything valuable I can find

@rtcpw
Copy link

rtcpw commented Oct 19, 2025

I've also spent a bunch of time trying to figure this one out, and I currently believe this is a hard limitation of the Vue framework and there isn't much we can do unless the template ref API is extended in one way or another. But please do let me know if you can find any solution to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment