
How to Access a Child Component’s Ref with multi-root node (Fragment) in Vue 3
In this article we want to explain how you can access to your child component's ref if you have more than one root (Fragment).
Introduction
If you're used to Vue 2, you might remember that every component's template needed a single root element. In Vue 3, that's no longer necessary because of fragments. This means your components can now have multiple root elements without needing a wrapper.
<!-- Vue 2 -->
<template>
<div> <!-- wrapper 😫 -->
<h1>My Blog Post</h1>
<ArticleComponent>{{ content }}</ArticleComponent>
</div>
</template>
<!-- Vue 3 -->
<template>
<h1>My Blog Post</h1>
<ArticleComponent>{{ content }}</ArticleComponent>
</template>
That's very similar to Fragment
in React. However, Vue handles fragments behind the scenes. In fact, in Vue 3, you can think of the <template>
tag as a fragment.
The ref()
Problem
In Vue 2, we could easily set a ref
on a child component, and it would refer to both the wrapper element and the component instance.
But in Vue 3, when there’s no wrapper element, what does the ref
refer to? 🤔
If the child component uses the
Options API
or doesn't use<script setup>
, the ref will point to the child component's this, giving the parent full access to its properties and methods.
What if we use <script setup>
?
Components using
<script setup>
are private by default. To expose properties, we need to use thedefineExpose
macro.
Access To Children's Element
This is what happen when you have wrapper (single root) element:
<!-- Child -->
<template>
<div class="wrapper"> <!-- Root -->
<h1>My Blog Post</h1>
<ArticleComponent>{{ content }}</ArticleComponent>
</div>
</template>
<!-- Parent -->
<script setup lang="ts">
const childRef = ref()
onMounted(()=>{
console.log(childRef.value.$el); // <div class="wrapper">…</div>})
</script>
<template>
<Child ref="childRef" />
</template>
And when you have more than one root:
<!-- Child -->
<template>
<h1>My Blog Post</h1> <!-- Root 1 -->
<ArticleComponent>{{ content }}</ArticleComponent> <!-- Root 2 -->
</template>
<!-- Parent -->
<script setup lang="ts">
const childRef = ref()
onMounted(()=>{
console.log(childRef.value.$el); // #text})
</script>
<template>
<Child ref="childRef" />
</template>
Wait, what, what happened?
When we using Fragment(multiple nodes), Vue creates a text
node that wraps our child component root nodes.
When using Fragments in Vue 3, Vue inserts an empty text node at the beginning of the component as a marker, which is why $el returns a #text
node.
#text
is like a reference point that Vue uses internally.
Also I should mention that you have still access to component instance (if you don't use <script setup>
in child).
Solution
- Use Single Root Like this
- Use Template Refs + defineExpose
Using Template Refs + defineExpose
<!-- Child -->
<script setup lang="ts">
import { ref } from 'vue';
const h1Ref = ref()
const articleRef = ref()
defineExpose({
h1Ref,
articleRef
})
</script>
<template>
<h1 ref="h1Ref">My Blog Post</h1>
<ArticleComponent ref="articleRef">{{ content }}</ArticleComponent>
</template>
<!-- Parent -->
<script setup lang="ts">
const childRef = ref()
onMounted(()=>{
console.log(childRef.value); // {h1Ref: RefImpl, articleRef: RefImpl}
})
</script>
<template>
<Child ref="childRef" />
</template>
Now you have access to your refs
and all the things that you exposed by using defineExpose
.
7 key questions and answers about template refs
A ref in Vue is a way to directly access DOM elements or child component instances. It provides a reference that you can use to manipulate elements, call methods, or access component data programmatically.
Vue 2 requires a single root element in every component (must wrap everything in one element), while Vue 3 supports fragments, allowing multiple root nodes without a wrapper element.
In Vue 2, refs are accessed via this.$refs.myRef
. In Vue 3 with <script setup>
, you need to:
- Declare the ref using
const myRef = ref(null)
- Access it using
myRef.value
- Use
defineExpose
if you want to make refs available to parent components.
In Vue 2, $el
refers to the root DOM element of the component (since there must be one). In Vue 3 with fragments, $el
points to an empty text node that Vue creates as a marker, since there might be multiple root elements.
When using <script setup>
, you must explicitly use defineExpose
to make any refs
, methods, or properties available to parent components. Without this, parent components cannot access the child's internal refs
.
- Use a single root element if you just need simple DOM access
- Use template refs with defineExpose when you need access to specific elements, making them explicitly available to the parent component
Both can be used for template references, but they have different approaches and benefits:
- Using ref (before Vue 3.5):
<script setup>
const input = ref(null) // name must match template ref
</script>
<template>
<input ref="input" />
</template>
- Using useTemplateRef (Vue 3.5+):
<script setup>
const input = useTemplateRef('my-input') // first argument matches ref value
</script>
<template>
<input ref="my-input" />
</template>
Key differences:
- useTemplateRef provides better TypeScript inference - it automatically infers the type of the element/component
- With ref, the variable name must match the template ref value
- With useTemplateRef, you explicitly specify the ref name as the first argument, allowing different variable and ref names
- Both approaches only provide access to the element after component mounting
- Both will be null before mounting or when element is unmounted (e.g., by v-if)