Reaching a Child
A template ref on a component gives you its instance, but only what defineExpose lists, and only when props and events cannot do the job.
const field = useTemplateRef<InstanceType<typeof SearchField>>('field')Sometimes a parent needs to tell a child to do something rather than pass it a value: focus an input, reset a form, start a video. Putting a ref attribute on a component tag gives the parent the child's instance, and from Vue 3.5 useTemplateRef('name') is how you read it, with the type inferred from the tag.
Closed by default
A component using <script setup> exposes nothing. The parent gets an instance with none of the child's internals on it. The child decides what is public with defineExpose({ focus, clear }); refs listed there arrive already unwrapped, so the parent reads .value once for the instance and not again for the ref.
Timing
The ref is null until the component is mounted, and null again after it unmounts, which includes anything behind a v-if. Calling into it from an event handler is safe because the user cannot click before mount; anything earlier needs a guard. Optional chaining is the usual way.
Why it should be rare
A ref is an escape hatch, not a design. It couples the parent to the child's internals, cannot be bound declaratively, and is invisible in the template. Prefer a prop for state the parent owns, an event for something the child reports, and a model for a value they share. Keep refs for imperative calls to the DOM that have no declarative form: focus, scroll, play, select.
Example
<!-- src/components/FilterBar.vue -->
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import SearchField from './SearchField.vue'
// useTemplateRef arrived in Vue 3.5. The string must match the
// ref attribute in the template.
const field = useTemplateRef<InstanceType<typeof SearchField>>('field')
const query = ref('')
function startOver() {
query.value = ''
// null before mount and after unmount, so guard the call.
field.value?.selectAll()
}
</script>
<template>
<div>
<search-field ref="field" v-model="query" />
<button type="button" @click="startOver">Start over</button>
<p>Filtering on: {{ query || 'everything' }}</p>
</div>
</template>
<style scoped>
div {
display: grid;
gap: 0.5rem;
}
</style>When to use it
- Focus the first field of a form after the user opens it from a button.
- Reset a complex child form from a parent's Cancel button without threading a counter prop through it.
- Select the text in a search box so the next keystroke replaces it.
- Call play or pause on a child that wraps a video element.
More examples
The child deciding what is public
A search input whose text selects itself when the parent asks. Two kinds of ref appear here: one on a DOM element, which gives the element, and the parent's one on this component, which gives whatever defineExpose lists. Expose the smallest surface you can; everything in that object becomes API that other components may depend on. defineExpose must run before any await in the script.
<!-- src/components/SearchField.vue -->
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const model = defineModel<string>({ default: '' })
const input = useTemplateRef<HTMLInputElement>('input')
// Private: not exposed, so not reachable from the parent.
function normalise(value: string) {
return value.trim().toLowerCase()
}
function selectAll() {
input.value?.focus()
input.value?.select()
}
// Only this name reaches a parent's ref.
defineExpose({ selectAll })
</script>
<template>
<input ref="input" v-model="model" type="search" />
</template>
<style scoped>
input {
width: 100%;
}
</style>Why it is null at first
Setup code runs before the template is mounted, so reading the ref at the top level of the script always gives null. Two consequences: do the work in an event handler or a lifecycle hook that runs after mount, and treat the value as nullable for the whole life of the component, because a v-if or a route change can take the child away again.
// Inside a parent's <script setup>, conceptually:
let instance: { selectAll: () => void } | null = null
console.log(instance)
// Output: null
// Vue assigns the instance when the child mounts.
instance = { selectAll: () => {} }
console.log(typeof instance.selectAll)
// Output: function
// And clears it when the child unmounts, e.g. a v-if turning false.
instance = null
console.log(instance)
// Output: nullThe same job without a ref
The version to try first. State the parent owns travels down as a model, and anything the child notices travels up as an event, so the parent never touches the child's internals and both can be tested on their own. Keep the ref for the cases this cannot express, which in practice means DOM calls such as focus, select, scrollIntoView and play.
<!-- src/components/FilterBarDeclarative.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import SearchField from './SearchField.vue'
const query = ref('')
const suggestions = ref<string[]>([])
// No ref into the child: the parent owns the value and reacts
// to what the child reports.
function onSubmitted(value: string) {
suggestions.value = value === '' ? [] : [`${value} in titles`]
}
</script>
<template>
<div>
<search-field v-model="query" @submitted="onSubmitted" />
<ul>
<li v-for="suggestion in suggestions" :key="suggestion">
{{ suggestion }}
</li>
</ul>
</div>
</template>
<style scoped>
div {
display: grid;
gap: 0.5rem;
}
</style>
Discussion