Skip to content

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:

vue
<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:

typescript
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:

vue
<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:

vue
<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:

vue
<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:

vue
<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

  1. 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.

  2. Refs in v-for loops: Use the function syntax :ref="el => { if (el) elements[i] = el }" instead of the array syntax for better reliability.

  3. Timing issues: If you need to ensure all child components are rendered, use nextTick():

javascript
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+ useTemplateRef when 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.