Skip to content

Instantly share code, notes, and snippets.

@AlexVipond
Last active July 17, 2024 19:11
Show Gist options
  • Save AlexVipond/94892f017e122f10b33887839a1a96b3 to your computer and use it in GitHub Desktop.
Save AlexVipond/94892f017e122f10b33887839a1a96b3 to your computer and use it in GitHub Desktop.
Function refs vs. ref objects

Function refs vs. ref objects

Great question I got on Twitter: what are the benefits of returning a function ref from a Vue 3 composable, instead of returning the ref object directly?

Here are two SFC playgrounds to outline the two alternatives:

Both examples are set up to capture a DOM element from the page and log it to the console. If you check the console, you'll see that both examples work perfectly.

Now let's try something more complex: capturing an array of elements rendered by v-for.

Two more SFC playgrounds to see how that's done:

Again, if you check the console, you can see that both examples are perfectly logging an array of your v-for elements.

This actually didn't work when I originally started working with function refs. Function refs used to be the only available solution in Vue 3 for capturing v-for elements and using them in composables or in script setup. But in Vue 3.2.25, better support for v-for ref objects was introduced, and I was pumped!

The bad news, and the main reason I continue to work with function refs when capturing arrays, is that the order of elements stored in a ref object isn't guaranteed to match the order of elements in the DOM. An old tweet thread of mine has more info on that.

Here's an SFC playground illustrating the problem.

And here's an SFC playground showing how a proper function ref seamlessly solves the problem.

Finally, let's consider the case of grids, rendered by nested v-for. Here's a quick code example of what I mean:

<script setup>
// We'll loop over this grid metadata with a nested `v-for` loops.
// The top-level loop will render each row, and the nested loop will
// render each cell in a given row.
const gridMetadata = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
]
</script>

<template>
  <!-- Top level renders the rows -->
  <div
    v-for="(row, rowIndex) in gridMetadata"
    :key="row[0]"
    style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem;"
  >
    <!-- Nested v-for renders the cells in a row -->
    <div
      v-for="(column, columnIndex) in row"
      :key="column"
    >
      {{ column }}
    </div> 
  </div>
</template>

Here are SFC playgrounds showing how function refs and ref objects perform on this challenge:

Note how the function ref is able to store elements in custom data structures. You can store grid elements, for example, in any number of different ways:

  • An array of arrays, matching the original grid metadata structure in our example
  • A Map, where the key is the x and y coordinates of the element in the grid, and the value is the the element itself. For example, you could call myMap.value.get({ x: 3, y: 2 }) to retrieve the element at those coordinates
  • A flat array
  • Anything you want! Whatever makes sense for your codebase will work, and since the logic is all contained in a function ref, you can change the internal data structure at any time without breaking users' code.

The ref object, on the other hand, just puts all the elements into a flat array.

You don't have any choice over how the elements get stored, and that can make the DOM much more difficult to work with when you're writing a complex composable, e.g. a keyboard accessible grid widget, or a keyboard accessible calendar or datepicker.

In conclusion

Ref objects are perfectly fine when:

  • You're just capturing a single element, or you're capturing an array of elements that won't get sorted, filtered, reordered, or otherwise changed reactively
  • You don't need to store elements in a custom data structure
  • You're not writing a full library of composables, where external consistency (i.e. always returning function refs to other developers) is more valuable than internal simplicity (i.e. taking the opportunity to use ref objects so you don't have to set up the additional function ref)

In all other situations, function refs are a better bet.

Cherry on top: you can avoid pretty much all function ref implementation complexity in your own composables by importing useElementApi from @baleada/vue-features, and using that composable to create and manage all your function refs for you.

More info in this video: https://www.youtube.com/watch?v=TYKD2GYGfs8&list=PLHP34VGeo17fW2FZ3v2Fptf7kSTTn8Jab

A note for React users

React has a feature called callback refs, which is essentially the same thing as function refs in Vue 3. In React, though, callback refs are the only way to collect elements from a rendered list; there's no alternative.

Vue 3 users who also use React might prefer using function refs as often as possible, just to keep the DX more consistent between the two frameworks, and maximize the chance that code written for one framework will need little or no change to work in the other framework.

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