Make Vue's template refs clean & simple
Tags: vue, refactorit
Published: 2023-11-05 04:17:49

Sometimes in a Vue Project, we may need direct access to the DOM elements in the template. A common usecase is that when we define expose ([defineExposes](https://vuejs.org/api/sfc-script-setup.html#defineexpose)) to a component, the parent component needs to access that component to call the exposed functions or attributes. When I use Vue's template refs for the first time, I feel something strange. This is an example:
The template:
```html
<template>
  <input ref="input" />
</template>
```
The setup script:
```ts
<script setup>
import { ref, onMounted } from 'vue'

// declare a ref to hold the element reference
// the name must match template ref value
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>
```
I have 2 questions here:
+ Question 1: Why the 'input' in the template is a const string but the 'input' in the setup script is a ref variable? Why we cannot pass the ref variable like `:ref="input"` instead of always keeping in mind that we need to make the variable name in the script and the string in the template are the same? Yeah, if you love Typescript, you will feel this is so weird. 
+ Question 2: Why do we need to call input.value to access the component? For most cases, this template ref usually is unchanged, so calling .value is unnecessary. We don't need it to be a changeable ref variable, we just want it to be a constant.
After trying some ways like using reactive, const,... I finally made a util for this problem, just 10 line codes.
```ts
import { reactive } from 'vue'

export const useRefs = <T extends object>() => {
  const refs = reactive<T>({} as T)
  const toRef = (refName: keyof T) => (el: any) => ((refs as T)[refName as keyof T] = el)

  return {
    refs,
    toRef,
  }
}
```
This is how I use it:
```vue
<template>
  <input :ref="toRef('input')" />
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import { useRefs } from '@common/utils/useRefs'

const { refs, toRef } = useRefs<{
  input: InstanceType<typeof HTMLInputElement>
}>()

onMounted(() => {
  refs.input.focus()
})
</script>
```

There are some pros of the above solution:
+ The ref name 'input' is just a const key of an object now. IDE can understand it easily, so IDE typing suggestions work like a charm. For example:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nf73wj5yfax1d5nxgt18.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6ktrwp4v7l5ryv7flq2.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pai0hlyop7a3zu1pkfnx.png)

+ Not only IDE, but the compiler also works better. It can validate component exposes. For example when I mistype the exposed function's name from focus() to focuses():

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8uld6z9uzx4d6yxej0u9.png)

With just a few lines of code, my problem with Vue's template refs became zero. Less IDE warnings, fewer unknown mistakes, and more safe type checking. This solution makes me more comfortable when working with template refs. Hope this will help you too. If you have a better solution, please tell me about that.

I also make a Gist here if anyone needs this: 
https://gist.github.com/iamsonnn/dbf6d8407a0959f7dc66f6d24231799c

Thank you for your reading!