With Vue 3’s <script setup>, the template, state, and behavior can live together, making a component more compact and easier to read while also giving better TypeScript support. A child component, however, does not open its internals to the parent automatically. To make specific values or methods available, the child declares them through the defineExpose compiler macro.
The following child component exposes count and increment:
<template>
<div>
<button @click="increment">{{ count }}</button>
</div>
</template>
<script setup>
import { ref, defineExpose } from "vue";
const count = ref(0);
const increment = () => {
count.value++;
};
defineExpose({
count,
increment,
});
</script>
Only the pieces passed to defineExpose are reachable from outside. The parent can keep a template ref to the child instance and use that ref to read the exposed data.
<template>
<div>
<ChildComponent ref="child" />
<button @click="logChildCount">Log Child Count</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import ChildComponent from "./ChildComponent.vue";
const child = ref(null);
const logChildCount = () => {
if (child.value) {
console.log(child.value.count.value);
}
};
</script>
In this parent, child is a ref that points to the child component. Once the child is mounted and the ref has a value, child.value gives access to the exposed API; child.value.count.value then reads the child’s count ref. The click handler logs that value.
A few constraints matter here. Compiler macros like defineExpose work only inside <script setup>, and they can be used without importing them. The four macros discussed in this context are defineProps, defineEmits, defineExpose, and withDefaults.
Accessing child data this way can make communication between parent and child direct and flexible. It also increases coupling between the components, so using a template ref plus defineExpose should be a deliberate choice rather than a default pattern for every child.