skip to content

In Vue 3, a parent holds a template ref to a `<script setup>` child and calls its `reset()`; why does that fail until the child uses defineExpose?

level: middleimportance: should knowfreq 55%

answer

  1. private by default
  2. restricted instance proxy
  3. defineExpose publishes an API
  4. exposed refs auto-unwrap
  5. call it before any await

basics

~20 s

A script setup component is closed by default: a parent's template ref receives a proxy that exposes none of its setup bindings. The child must call defineExpose({ reset }) before any await, and the parent then calls childRef.value.reset() after mount.

solid answer

~40 s

A `ref` on a component gives the parent that component's **public instance**. For a `<script setup>` child, that instance is **private by default**: its setup variables and functions are invisible through the ref, so `childRef.value.reset` is `undefined`. The child opts in with `defineExpose({ reset })`; the parent then sees exactly those members, with exposed refs automatically unwrapped, plus built-in instance properties such as `$el`. Call `defineExpose` before any `await` in `<script setup>`, or members exposed after the await are not accessible. The parent reads the ref only after mount, for example `onMounted(() => form.value?.reset())` or in a click handler. Keep the exposed surface small: it is a public API that other components will depend on.

code

vue · 18 lines
vue
<script setup>
// ProfileForm.vue
import { ref } from 'vue'

const name = ref('')
const dirty = ref(false)

function reset() {
  name.value = ''
  dirty.value = false
}

defineExpose({ reset, dirty })
</script>

<template>
  <input v-model="name" @input="dirty = true" />
</template>

go deeper

for a junior

Know that a ref on a component gives you its instance, and a script setup child must use defineExpose.

for a middle

Explain what a parent's ref can and cannot see, ref unwrapping on exposed members, the await rule, and when the ref is filled.

for a senior

Decide when an exposed method is justified over props and events, and keep the exposed surface minimal.

for a principal

Set library-wide rules for which imperative methods components may expose and how they are versioned and documented.

## Refs on components The `ref` attribute works on components as well as elements. On a component, the value is the child's **public instance**, not a DOM node: ```vue <script setup> import { useTemplateRef } from 'vue' import ProfileForm from './ProfileForm.vue' const form = useTemplateRef('form') function onCancel() { form.value?.reset() } </script> <template> <ProfileForm ref="form" /> <button @click="onCancel">Cancel</button> </template> ``` With an Options API child, the public instance exposes its data, computed properties and methods, unless the child restricts them with the `expose` option. A `<script setup>` child behaves differently. ## Script setup components are closed by default The Vue guide says components using `<script setup>` are **private by default**. Unless the child calls `defineExpose`, the instance a parent receives resolves none of the child's setup bindings. What the parent can reach is: - the members passed to `defineExpose`, and - Vue's built-in public properties, such as `$el`, `$props` and `$emit`. It does **not** reach the child's local refs, computed values or functions. So without `defineExpose`, `form.value.reset` is `undefined` and calling it throws a TypeError. ## Publishing an API with defineExpose ```vue <script setup> import { ref } from 'vue' const name = ref('') const dirty = ref(false) function reset() { name.value = '' dirty.value = false } defineExpose({ reset, dirty }) </script> ``` Rules to know: 1. **Exposed refs are unwrapped.** The parent reads `form.value.dirty` as a boolean, not `form.value.dirty.value`. 2. **Call it before any `await`.** If `<script setup>` uses top-level `await`, members exposed after the await are not accessible to parents. 3. **Only what you pass is visible.** Adding a helper function later does not leak it. 4. `defineExpose` is a compiler macro, so it needs no import. ## When the parent can call it The component ref follows the same timing as element refs: - `null` during the parent's setup and the first render; - the child's exposed proxy once the child has mounted, which is before the parent's `onMounted` runs; - `null` again if the child is removed by `v-if`. So call exposed methods in `onMounted`, event handlers or later, and use optional chaining when the child may be absent. ## Design guidance Exposing methods creates an **imperative channel** from parent to child. That channel is useful for actions that are awkward as data, such as `reset()`, `focus()` or `validate()`, but it has costs: | Choice | Good for | Cost | |---|---|---| | Props and events | state that the parent owns | more plumbing for one-off actions | | Exposed method | commands like reset or focus | hidden coupling; harder to test from the template | | Exposed state | read-only status such as `dirty` | parents may start relying on internals | Prefer exposing **a few intention-revealing methods** over whole internal refs. If you expose a ref, the parent can write to it, which bypasses the child's own logic. ## Common mistakes - Expecting every setup binding to be visible, as it is with an Options API child. - Reading `childRef.value` during the parent's setup. - Exposing after `await`, then chasing an `undefined` method. - Using `$el` to reach inside the child's DOM instead of exposing a `focus()` method; for a multi-root child `$el` is not even its first element. ## The reset scenario, argued both ways For a form that a parent's Cancel button must clear, two designs compete: 1. **Exposed method.** The child keeps its own draft state and exposes `reset()`. The parent calls `form.value?.reset()`. Simple, and the child's internals stay private. 2. **Lifted state.** The parent owns the values and passes them down, receiving changes through events or a component `v-model`. Cancel then just restores the parent's copy, and no ref is needed. Choose the exposed method when the state is genuinely the child's business and the parent only issues commands. Choose lifted state when the parent must also read, validate or persist the values; exposing `dirty`, `values` and `setValues()` from a child is usually a sign the state belongs one level up.

  • In Vue 3, if a `<script setup>` child exposes `dirty` (a ref), how does the parent read it?
    As `childRef.value.dirty`, already unwrapped. The exposed object is wrapped so refs unwrap on access, just as on a normal component instance. The parent could also assign to it, which is why exposing commands like `reset()` is usually safer than exposing raw state.
  • What can a Vue 3 parent still read from a `<script setup>` child that exposes nothing?
    Only built-in public instance properties such as `$el`, `$props` and `$emit`. Setup variables and functions stay hidden. Relying on `$el` couples the parent to the child's markup, and for a multi-root child it does not point at a single root element, so an exposed method is better.

A script setup child is like an office behind a service window: visitors can read the building's public directory ($el, $props) and use whatever the child places at the window with defineExpose, but never reach the desks behind it.

saying these in an interview costs you the question

  • Expects every script setup variable to be visible through a component ref
  • Reads childRef.value.dirty.value for an exposed ref
  • Calls defineExpose after a top-level await
  • Calls exposed methods during the parent's setup
  • Exposes whole internal state instead of a few commands