Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/component-ref-requires-defineexpose.md
1---2title: Component Refs Require defineExpose with Script Setup3impact: HIGH4impactDescription: Parent components cannot access child ref properties unless explicitly exposed5type: gotcha6tags: [vue3, template-refs, script-setup, defineExpose, component-communication]7---89# Component Refs Require defineExpose with Script Setup1011**Impact: HIGH** - Components using `<script setup>` are private by default. A parent component using a template ref to access a child will get an empty object unless the child explicitly exposes properties using `defineExpose()`. This is a fundamental change from Options API behavior.1213This catches many developers off-guard when migrating from Options API, where `this.$refs.child` gave full access to the child instance.1415## Task Checklist1617- [ ] Use `defineExpose()` to explicitly expose properties/methods to parent refs18- [ ] Only expose what's necessary - keep component internals private19- [ ] Document exposed APIs as they form your component's public interface20- [ ] Prefer props/emit for parent-child communication; use refs sparingly21- [ ] Call defineExpose before any await operation (see async caveat)2223**Incorrect:**24```vue25<!-- ChildComponent.vue -->26<script setup>27import { ref } from 'vue'2829const count = ref(0)30const internalState = ref('private')3132function increment() {33count.value++34}3536function reset() {37count.value = 038}3940// WRONG: Nothing exposed - parent ref sees empty object41</script>4243<template>44<div>{{ count }}</div>45</template>46```4748```vue49<!-- ParentComponent.vue -->50<script setup>51import { ref, onMounted } from 'vue'52import ChildComponent from './ChildComponent.vue'5354const childRef = ref(null)5556onMounted(() => {57// WRONG: childRef.value is {} - empty object!58console.log(childRef.value.count) // undefined59childRef.value.increment() // TypeError: not a function60})61</script>6263<template>64<ChildComponent ref="childRef" />65</template>66```6768**Correct:**69```vue70<!-- ChildComponent.vue -->71<script setup>72import { ref } from 'vue'7374const count = ref(0)75const internalState = ref('private') // Keep this private7677function increment() {78count.value++79}8081function reset() {82count.value = 083}8485// CORRECT: Explicitly expose public API86defineExpose({87count, // Expose the ref88increment, // Expose methods89reset90// internalState NOT exposed - stays private91})92</script>9394<template>95<div>{{ count }}</div>96</template>97```9899```vue100<!-- ParentComponent.vue -->101<script setup>102import { ref, onMounted } from 'vue'103import ChildComponent from './ChildComponent.vue'104105const childRef = ref(null)106107onMounted(() => {108// CORRECT: Can access exposed properties109console.log(childRef.value.count) // 0110childRef.value.increment() // Works!111112// internalState is not accessible (private)113console.log(childRef.value.internalState) // undefined114})115</script>116117<template>118<ChildComponent ref="childRef" />119</template>120```121122```vue123<!-- Input wrapper example - exposing native element -->124<script setup>125import { ref } from 'vue'126127const inputEl = ref(null)128129// Expose the native input for parent to access (e.g., for focus)130defineExpose({131focus: () => inputEl.value?.focus(),132blur: () => inputEl.value?.blur(),133// Or expose the element directly134el: inputEl135})136</script>137138<template>139<input ref="inputEl" v-bind="$attrs" />140</template>141```142143```javascript144// Options API equivalent using expose option145export default {146expose: ['count', 'increment', 'reset'],147data() {148return {149count: 0,150internalState: 'private'151}152},153methods: {154increment() { this.count++ },155reset() { this.count = 0 }156}157}158```159160## Best Practice Reminder161162Component refs create tight coupling between parent and child. Prefer standard patterns:163164```vue165<!-- PREFERRED: Use props and emit for communication -->166<script setup>167const props = defineProps(['modelValue'])168const emit = defineEmits(['update:modelValue'])169</script>170171<!-- Only use refs for imperative actions like focus(), scrollTo(), etc. -->172```173174## Reference175- [Vue.js Component Refs](https://vuejs.org/guide/essentials/template-refs.html#ref-on-component)176- [Script Setup - defineExpose](https://vuejs.org/api/sfc-script-setup.html#defineexpose)177