Template Refs in Vue 3 Composition API
Problem
When migrating from Vue 2's Options API to Vue 3's Composition API, developers often encounter challenges accessing template references ($refs). The traditional this.$refs approach no longer works directly in the setup() function, leading to undefined references when trying to access child components or DOM elements.
The core issue is that the Composition API's setup() function executes before the component instance is fully created and mounted, making the traditional this context unavailable for accessing template references.
Solutions
Basic Template Refs with Composition API
The recommended approach in Vue 3's Composition API is to use the ref function to create reactive references:
<template>
<comp-foo />
<comp-bar ref="table"/>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const table = ref(null);
onMounted(() => {
console.log(table.value); // Now accessible
if (table.value) {
table.value.tempMethod();
}
});
return { table };
}
};
</script>TypeScript Support
For TypeScript users, you can add type annotations to your template refs:
import { ref, onMounted } from 'vue';
import type { ComponentPublicInstance } from 'vue';
// For DOM elements
const tableElement = ref<HTMLDivElement | null>(null);
// For component instances
const tableComponent = ref<ComponentPublicInstance | null>(null);WARNING
Always check that the ref value is not null before accessing its properties or methods, as the element/component might not be mounted yet.
Handling Conditional Rendering
When your ref element is conditionally rendered, the ref will only be populated when the component is actually mounted:
<template>
<div v-if="isVisible">
<div ref="table"/>
</div>
</template>
<script>
import { ref, onMounted, watch } from 'vue';
export default {
setup() {
const table = ref(null);
const isVisible = ref(false);
watch(isVisible, (newValue) => {
if (newValue) {
// Table ref will now be available
console.log(table.value);
}
});
return { table, isVisible };
}
};
</script>Working with Multiple Refs in v-for Loops
When using v-for with template refs, you need a different approach to collect multiple references:
<template>
<div v-for="(item, i) in items"
:ref="el => { if (el) elements[i] = el }"
:key="item.id">
<div>ID: {{ item.id }}</div>
<div>Name: {{ item.name }}</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const items = [
{id: 1, name: "item name 1"},
{id: 2, name: "item name 2"},
{id: 3, name: "item name 3"},
];
const elements = ref([]);
onMounted(() => {
console.log(elements.value.map(el => el.textContent));
});
return { elements, items };
}
};
</script>INFO
The function syntax for refs in v-for loops ensures you capture all elements correctly, especially in Vue versions between 3.2.25 and 3.2.31 where the array ref approach may not work properly.
Using Vue 3.5+ useTemplateRef
Vue 3.5 introduced the useTemplateRef function, which provides a more ergonomic way to work with template refs:
<template>
<comp-bar ref="table"/>
</template>
<script setup>
import { useTemplateRef } from 'vue';
const table = useTemplateRef('table');
// The ref will be automatically available when the component mounts
</script>Alternative: Using getCurrentInstance()
While not generally recommended (as it's considered an internal API), you can access refs through the component instance:
<script>
import { onMounted, getCurrentInstance } from 'vue';
export default {
setup() {
onMounted(() => {
const instance = getCurrentInstance();
console.log(instance.ctx.$refs.table);
});
}
};
</script>DANGER
Using getCurrentInstance() is generally discouraged as it exposes Vue's internal API which may change in future versions. Prefer the standard ref() approach for better maintainability.
Common Pitfalls and Solutions
Ref is null on first render: Template refs are only populated after the component mounts. Use
onMounted()or watch for changes if dealing with conditional rendering.Refs in v-for loops: Use the function syntax
:ref="el => { if (el) elements[i] = el }"instead of the array syntax for better reliability.Timing issues: If you need to ensure all child components are rendered, use
nextTick():
import { nextTick, onMounted } from 'vue';
onMounted(async () => {
await nextTick();
// All child components should now be mounted
console.log(table.value);
});Best Practices
- Use the standard
ref()approach for most use cases - Add proper TypeScript types for better development experience
- Always check if refs are not null before accessing their properties
- Prefer the latest Vue 3.5+
useTemplateRefwhen available - Avoid relying on
getCurrentInstance()for production code
By following these patterns, you can effectively work with template references in Vue 3's Composition API while maintaining clean, maintainable code.