codejump

AcademyVue

Template Refs: Reaching the Real DOM

A template ref is null until onMounted — the component has to exist in the DOM before anything can point at it.

2 min readUpdated

3 exercises

Waiting at the end of this page.

A template ref is a plain ref that Vue fills in for you

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const inputEl = ref<HTMLInputElement | null>(null)

onMounted(() => {
  inputEl.value?.focus()
})
</script>

<template>
  <input ref="inputEl" />
</template>

Naming a ref in the template — ref="inputEl" — tells Vue to assign the real DOM element to a script setup variable of the same name once it exists. Before that point, inputEl.value is null: there is no DOM node yet, and code that reaches for it before mounting gets nothing.

The DOM does not exist yet during <script setup>'s own execution

JavaScript
const inputEl = ref<HTMLInputElement | null>(null)

// BUG: this line runs while <script setup> executes — before the template
// has been compiled into real DOM nodes. inputEl.value is still null here.
inputEl.value?.focus()

The ?. keeps this from throwing, which is exactly what makes the bug quiet: nothing crashes, the input just never receives focus, and there is no error message pointing at the cause. onMounted is the callback Vue guarantees runs only after the component's DOM has been created and inserted — anything that needs a real element has to live inside it (or later: onUpdated, if it needs to run after every subsequent re-render too).

One ref name, one element — for many, use a function ref

A string ref="name" can only ever point at one element. Inside v-for, use a function instead, called once per rendered item with that item's element:

vue
<script setup lang="ts">
const items = ref(['Apple', 'Banana', 'Cherry'])
const itemRefs = ref<HTMLLIElement[]>([])

function setItemRef(el: Element | null, index: number) {
  if (el) itemRefs.value[index] = el as HTMLLIElement
}
</script>

<template>
  <li v-for="(item, i) in items" :key="item" :ref="(el) => setItemRef(el as Element | null, i)">
    {{ item }}
  </li>
</template>

Vue calls the function with null when an element is about to unmount too, which is the hook to remove it from the array again in a longer-lived list — omitted here since items never shrinks, but worth knowing the callback fires both ways.

Refs on components give you the child's exposed API

ref="childComponent" on a component (rather than a plain element) gives you the child's public surface: whatever it explicitly exposes with defineExpose({ ... }). A <script setup> component exposes nothing by default — deliberately, since a parent reaching into a child's internal refs and functions is exactly the kind of tight coupling props and emits exist to avoid. defineExpose is the escape hatch for the genuine exceptions (calling focus() or reset() on a child form, say), used narrowly rather than as a habit.

What interviewers check

Whether you know why a template ref starts null — not just that it does. "Because the DOM does not exist until the component mounts" is the answer that generalizes to onUpdated, nextTick, and every other "why do I have to wait" question Vue's rendering model raises.

Now practice it

2 quizzes · 1 drill

Reading this page is the cheap half. These are the exercises that make you use it.