Chapter 93: Vue ‘expose’ Option
The defineExpose macro (often just called the expose option or expose API)
This is not an option like props, data, computed or methods in the old Options API. It is a special macro (a function you call at the top level of <script setup>) that lets you choose which variables / functions from inside your <script setup> block will be publicly accessible to the parent component when it uses a template ref (ref=”myChild”).
In short:
By default, nothing inside <script setup> is exposed to the parent via template ref. defineExpose() is how you explicitly say: “Parent, you are allowed to reach in and touch these things.”
Why defineExpose exists – The Big Problem It Solves
Before Vue 3.2 / early <script setup> days, people were very confused:
- In Options API → parent could do this.$refs.child.someMethod() because everything in methods was automatically exposed.
- In <script setup> → nothing is exposed by default → this.$refs.child is basically an empty object → parents could not call child methods or read child refs/state.
→ Developers started doing ugly hacks:
- this.$refs.child.$el (DOM access — bad)
- emitting events everywhere (overkill)
- using provide/inject globally (messy)
defineExpose fixes this cleanly and safely.
Basic Syntax – How to Use It
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<script setup> import { ref } from 'vue' const count = ref(0) const inputEl = ref(null) function focusInput() { inputEl.value?.focus() } function reset() { count.value = 0 } // This is the expose line – only these are visible to parent defineExpose({ count, // exposes the ref itself (parent can do child.count.value) reset, // exposes the function focusInput // exposes the function }) </script> <template> <div> <input ref="inputEl" type="text" /> <p>Child count: {{ count }}</p> </div> </template> |
Parent usage
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<template> <div> <ChildComponent ref="childRef" /> <button @click="childRef.reset()">Reset child count</button> <button @click="childRef.focusInput()">Focus child input</button> <!-- Can also read the ref directly --> <p>Child count from parent: {{ childRef?.count?.value }}</p> </div> </template> <script setup> import ChildComponent from './ChildComponent.vue' import { ref } from 'vue' const childRef = ref(null) </script> |
Important Rules & Gotchas (2026 Must-Know)
| Rule / Gotcha | Correct Behavior / Best Practice |
|---|---|
| defineExpose only works in <script setup> | In Options API → everything in methods is auto-exposed (no need for defineExpose) |
| Only what you explicitly expose is accessible | const secret = ref(42) → parent cannot reach it unless you expose it |
| You expose references / functions | Expose count (the ref), not count.value (the raw number) |
| Parent accesses via ref.value | childRef.value.reset() or childRef.value.count.value |
| TypeScript support | Excellent — defineExpose<{ reset: () => void; count: Ref<number> }>() |
| Can expose computed? | Yes — defineExpose({ fullName }) if fullName = computed(…) |
| Can expose props? | Yes — but usually pointless (parent already knows them) |
| Still needed in 2026? | Yes — whenever parent needs to call child methods or read child refs/state |
Real-World Example – Modal Component with Exposed Methods
Child: Modal.vue (very common pattern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
<template> <Teleport to="body"> <Transition name="modal"> <div v-if="isVisible" class="modal-overlay" @click.self="close"> <div class="modal-content"> <slot name="header"> <h2>Confirmation</h2> </slot> <slot /> <div class="modal-footer"> <slot name="footer"> <button @click="close">Cancel</button> <button @click="confirm">Confirm</button> </slot> </div> </div> </div> </Transition> </Teleport> </template> <script setup lang="ts"> import { ref, watch } from 'vue' const props = defineProps<{ modelValue: boolean }>() const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>() const isVisible = ref(props.modelValue) // Sync with v-model watch(() => props.modelValue, val => { isVisible.value = val }) function close() { emit('update:modelValue', false) } function confirm() { emit('update:modelValue', false) // You can emit another event if needed: emit('confirm') } // ── Expose public API to parent ─────────────────────────────── defineExpose({ open: () => { isVisible.value = true }, close, confirm, isVisible // expose the ref so parent can watch/read it }) </script> <style scoped> .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .modal-content { background: white; padding: 2rem; border-radius: 12px; max-width: 500px; width: 90%; } .modal-footer { margin-top: 1.5rem; text-align: right; } .modal-enter-active, .modal-leave-active { transition: all 0.3s ease; } .modal-enter-from, .modal-leave-to { opacity: 0; transform: scale(0.9); } </style> |
Parent – using exposed methods
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<template> <div> <button @click="childRef?.open()">Open Modal</button> <Modal ref="childRef" v-model="showModal" @confirm="handleConfirm"> <template #header> <h2>Delete Project?</h2> </template> <p>This action cannot be undone.</p> </Modal> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import Modal from '@/components/Modal.vue' const showModal = ref(false) const childRef = ref(null) function handleConfirm() { console.log('Confirmed deletion!') showModal.value = false } </script> |
→ Parent can call childRef.open(), childRef.close(), childRef.confirm() directly
Quick Summary Table – defineExpose in 2026
| Question | Answer / Best Practice |
|---|---|
| Where is it used? | Only inside <script setup> — not in Options API |
| What does it expose? | Variables (ref/reactive/computed), functions, objects — anything you want parent to access |
| Does parent get reactivity? | Yes — if you expose a ref or reactive, parent can watch/read .value |
| TypeScript support | Excellent — defineExpose<{ open: () => void; isVisible: Ref<boolean> }>() |
| Still needed in 2026? | Yes — whenever parent needs to call child methods or read child internal state/refs |
| Alternative if no expose? | Emit events upward — parent handles everything (cleaner in many cases) |
Pro Tips from Real Projects (Hyderabad 2026)
- Use defineExposeonly when necessary — prefer events (emit) for most parent-child communication
- Expose functions (open/close/reset/validate) much more often than exposing state (refs)
- Exposing state (like isVisible) → useful when parent wants to watch child state
- In UI libraries (modal, drawer, dropdown) → defineExpose({ open, close, toggle }) is standard
- Never expose everything — keep public API minimal & documented
- Combine with template ref (ref=”child”) — parent does child.value.open()
Your mini practice task:
- Create Modal.vue exactly as above with defineExpose({ open, close })
- Use it in parent with ref=”modalRef” → add buttons “Open Modal” and “Close Modal”
- Call modalRef.value.open() and modalRef.value.close() from parent
- Add isVisible to expose → watch it in parent and log changes
Any part confusing? Want full examples for:
- Modal with defineExpose + TypeScript typing?
- Exposing computed values & reactive objects?
- defineExpose vs emit events comparison?
- Real UI library component (drawer, dialog, tooltip) with expose?
Just tell me — we’ll build the next clean, exposed-API component together 🚀
