auf Kindkomponenten richtig einsetzen
Eine Komponente in script setup ist standardmäßig eine geschlossene Blackbox: Weder ihre reaktiven Variablen noch ihre Funktionen sind vom Elternteil aus per Template-Ref erreichbar, selbst wenn der Ref korrekt gesetzt ist. Das ist bewusstes Design und schützt vor unkontrolliertem Zugriff auf interne Implementierungsdetails, lässt sich aber gezielt mit defineExpose aufheben, wenn ein Elternteil tatsächlich eine bestimmte Methode einer Kindkomponente direkt aufrufen muss, etwa um ein Formular zu validieren oder ein Modal zu öffnen. Wer versteht, wo dieser Zugriff sinnvoll ist und wo er sich besser durch Props und Events ersetzen ließe, vermeidet sowohl unnötig rigide als auch unnötig verworrene Komponentenschnittstellen.
Inhaltsverzeichnis
- 1. Warum script setup Komponenten standardmäßig gekapselt sind
- 2. defineExpose: Grundlagen und Syntax
- 3. Zugriff vom Elternteil per Template-Ref auf die Kindkomponente
- 4. Typisierung des Refs mit InstanceType und ComponentExposed
- 5. Praktisches Beispiel: ein Formular mit exponierter validate()-Methode
- 6. Praktisches Beispiel: ein Modal mit exponierten open()/close()-Methoden
- 7. Wann der Zugriff ein Anti-Pattern gegenüber Props/Events ist
- 8. defineExpose im Vergleich zur Options API (expose-Option)
- 9. Häufige Fehler und Fazit
- 10. Zusammenfassung
- 11. FAQ
1. Warum script setup Komponenten standardmäßig gekapselt sind
In der klassischen Options API war eine Komponenteninstanz von außen weitgehend offen: Über einen Template-Ref ließ sich praktisch jede Data-Property und jede Methode direkt vom Elternteil aus ansprechen, was zwar flexibel war, aber auch dazu einlud, interne Implementierungsdetails einer Komponente von außen zu manipulieren, obwohl sie eigentlich als privat gedacht waren. script setup kehrt dieses Verhalten bewusst um: Standardmäßig ist die public instance einer script setup Komponente komplett leer, ein Template-Ref liefert also ein Objekt ohne zugreifbare Properties oder Methoden.
Diese Entscheidung folgt demselben Prinzip wie private Felder in objektorientierten Sprachen: Eine Komponente definiert selbst, welcher Teil ihrer internen Logik als öffentliche Schnittstelle gilt, statt dass jede lokale Variable automatisch nach außen sichtbar wird. Für die meisten Komponenten ist das kein Problem, weil die Kommunikation über Props nach unten und Events nach oben läuft. Erst wenn ein Elternteil wirklich imperativ auf eine Kindkomponente einwirken muss, etwa um eine bestimmte Aktion punktgenau auszulösen, wird die standardmäßige Kapselung zur relevanten Hürde.
2. defineExpose: Grundlagen und Syntax
defineExpose ist ein Compiler-Makro, das ausschließlich innerhalb von script setup verfügbar ist und ein Objekt mit denjenigen Properties und Methoden entgegennimmt, die tatsächlich nach außen sichtbar sein sollen. Der Aufruf defineExpose({ validate, reset }) macht genau diese beiden Funktionen auf der public instance verfügbar, während alle anderen lokalen Variablen, Funktionen und reaktiven Zustände weiterhin vollständig privat bleiben und vom Elternteil aus nicht erreichbar sind.
Wie defineProps oder defineEmits muss defineExpose direkt im obersten Bereich von script setup aufgerufen werden, nicht innerhalb einer Bedingung oder einer verschachtelten Funktion, weil der Compiler diesen Aufruf zur Kompilierzeit statisch analysiert. Wird defineExpose gar nicht aufgerufen, bleibt die Komponente wie beschrieben komplett gekapselt, was für die große Mehrheit aller Komponenten auch das gewünschte Standardverhalten ist.
<script setup lang="ts">
import { ref } from 'vue'
const errorMessage = ref('')
function validate(): boolean {
errorMessage.value = ''
// ... eigentliche Validierungslogik
return errorMessage.value === ''
}
function reset() {
errorMessage.value = ''
}
defineExpose({ validate, reset })
</script>
3. Zugriff vom Elternteil per Template-Ref auf die Kindkomponente
Um vom Elternteil aus auf die exponierten Methoden einer Kindkomponente zuzugreifen, wird zunächst wie gewohnt ein Template-Ref angelegt, entweder über ref-Attribut plus passenden ref()-Aufruf in script setup, oder in Vue 3.5 und neuer optional per useTemplateRef(). Der Name des ref-Attributs im Template muss dabei exakt dem Namen der lokalen Variable entsprechen, die den Ref hält, sonst bleibt formRef.value nach dem Mounten auf null stehen.
Sobald die Kindkomponente gemountet ist, liefert formRef.value genau das Objekt zurück, das die Kindkomponente per defineExpose freigegeben hat, in unserem Beispiel also ein Objekt mit validate und reset. Ein Aufruf wie formRef.value?.validate() im Elternteil ruft dann direkt die Methode der Kindkomponente auf, komplett unabhängig von jeder Prop- oder Event-Verbindung zwischen den beiden Komponenten, weshalb dieser Zugriffsweg als imperative API bezeichnet wird, im Gegensatz zur deklarativen Kommunikation über Props und Events.
<script setup lang="ts">
import { ref } from 'vue'
import FormChild from './FormChild.vue'
const formRef = ref<InstanceType<typeof FormChild> | null>(null)
function handleSubmit() {
if (formRef.value?.validate()) {
// Formular ist gültig, weiter zum Absenden
}
}
</script>
<template>
<FormChild ref="formRef" />
<button @click="handleSubmit">Absenden</button>
</template>
4. Typisierung des Refs mit InstanceType und ComponentExposed
Damit formRef.value im Elternteil korrekt typisiert ist und die IDE validate sowie reset per Autovervollständigung anbietet, wird der Ref typischerweise mit ref
Für generische Komponenten oder Fälle, in denen InstanceType nicht präzise genug ist, bietet die Community-Bibliothek vue-component-type-helpers den Typ ComponentExposed
5. Praktisches Beispiel: ein Formular mit exponierter validate()-Methode
Ein besonders häufiger, legitimer Anwendungsfall für defineExpose ist ein wiederverwendbares Formular-Feld oder eine ganze Formular-Sektion, die ihre eigene Validierungslogik kapselt, aber von einer übergeordneten Formular-Komponente zentral getriggert werden soll, wenn der Nutzer auf Absenden klickt. Jede Feld-Komponente exponiert dabei eine eigene validate()-Methode, die intern prüft, ob der aktuelle Wert gültig ist, und einen booleschen Wert zurückliefert.
Die übergeordnete Formular-Komponente hält für jedes Feld einen eigenen Template-Ref und ruft beim Absenden alle validate()-Methoden nacheinander auf, sammelt die Ergebnisse und blockiert den eigentlichen Submit, sobald mindestens eine Feld-Validierung fehlschlägt. Dieses Muster funktioniert deutlich robuster als eine zentrale, monolithische Validierungslogik im Elternteil, weil jedes Feld seine eigenen Validierungsregeln kapselt und trotzdem zentral orchestriert werden kann, ohne dass Validierungsdetails über Props nach außen dringen müssen.
<script setup lang="ts">
import { ref } from 'vue'
import FormField from './FormField.vue'
const nameFieldRef = ref<InstanceType<typeof FormField> | null>(null)
const emailFieldRef = ref<InstanceType<typeof FormField> | null>(null)
function submitForm() {
const isValid = [nameFieldRef, emailFieldRef].every(
(field) => field.value?.validate() === true,
)
if (isValid) {
// alle Felder gültig, Formular absenden
}
}
</script>
6. Praktisches Beispiel: ein Modal mit exponierten open()/close()-Methoden
Ein zweites klassisches Beispiel ist eine Modal-Komponente, deren Sichtbarkeit intern über einen lokalen, reaktiven Zustand gesteuert wird, aber vom Elternteil aus imperativ geöffnet und geschlossen werden soll, etwa als Reaktion auf einen Button-Klick an ganz anderer Stelle im Template. Statt die Sichtbarkeit als v-model-Prop nach außen zu reichen, exponiert das Modal stattdessen zwei einfache Methoden, open() und close(), die intern lediglich den lokalen isVisible-Zustand umschalten.
Dieser Ansatz eignet sich besonders gut, wenn das Modal selbst weitere interne Details verwaltet, die den Elternteil nicht interessieren sollten, etwa eine eingebaute Ein- und Ausblend-Animation, die beim Aufruf von close() erst abgewartet werden muss, bevor das Element tatsächlich aus dem DOM entfernt wird. Die imperative API aus open() und close() hält diese Details vollständig innerhalb der Modal-Komponente, während der Elternteil nur die beiden Methodennamen kennen muss, ohne sich um die interne Animationslogik zu kümmern.
7. Wann der Zugriff ein Anti-Pattern gegenüber Props/Events ist
defineExpose wird problematisch, sobald es genutzt wird, um regulären, reaktiven Datenfluss zu ersetzen, der eigentlich sauber über Props und Events abgebildet werden sollte. Wird beispielsweise ein reaktiver Zustand wie currentValue per defineExpose freigegeben, damit der Elternteil ihn direkt liest, statt ihn über ein Event nach außen zu emittieren, entsteht eine verdeckte, schwer nachvollziehbare Abhängigkeit: Änderungen im Kind wirken sich nur dann im Elternteil aus, wenn dieser aktiv und wiederholt formRef.value.currentValue abfragt, was der deklarativen, reaktiven Natur von Vue widerspricht.
Als Faustregel gilt: Für Daten, die sich über die Zeit ändern und im Elternteil reaktiv reflektiert werden sollen, sind Props nach unten und Events nach oben fast immer die bessere Wahl, weil Vues Reactivity-System diesen Datenfluss automatisch nachverfolgt. defineExpose eignet sich dagegen für punktuelle, imperative Aktionen wie validate(), open(), close() oder focus(), die einmalig ausgelöst werden und keinen kontinuierlichen, reaktiven Datenfluss darstellen. Wird die Grenze zwischen beiden Fällen verwischt, wird die Kindkomponente faktisch zu einer Extension des Elternteils statt einer eigenständigen, wiederverwendbaren Einheit mit klarer Schnittstelle.
8. defineExpose im Vergleich zur Options API (expose-Option)
In der Options API existiert ein ähnliches Konzept über die expose-Option, die als Array von Property- und Methodennamen angegeben wird, etwa expose: ['validate', 'reset']. Das Verhalten ist konzeptuell identisch zu defineExpose: Nur die dort genannten Namen werden auf der public instance sichtbar gemacht, alles andere bleibt für den Zugriff per Template-Ref unerreichbar, auch wenn es innerhalb der Komponente als Data-Property oder Methode existiert.
Der praktische Unterschied liegt vor allem in der Syntax und dem Zeitpunkt der Deklaration: Während die Options-API-Variante ein statisches Array von Strings erwartet, arbeitet defineExpose mit einem tatsächlichen Objekt aus lokal definierten Konstanten, was in TypeScript-Projekten eine präzisere, automatisch abgeleitete Typisierung der exponierten Schnittstelle ermöglicht, ohne dass Property-Namen als Strings dupliziert werden müssen und dabei auseinanderlaufen können.
9. Häufige Fehler und Fazit
Der häufigste Fehler ist, defineExpose komplett zu vergessen und sich anschließend zu wundern, warum formRef.value.validate im Elternteil undefined ist, obwohl die Methode in der Kindkomponente sichtbar existiert. Ein zweiter häufiger Fehler ist der Zugriff auf formRef.value, bevor die Kindkomponente tatsächlich gemountet ist, etwa in onMounted des Elternteils, wenn die Kindkomponente noch hinter einem v-if verborgen ist, was ebenfalls zu einem null-Wert führt und im Fehlerfall abgesichert werden muss.
Als Fazit gilt: defineExpose ist ein bewusst eng gehaltenes Werkzeug für punktuelle, imperative Schnittstellen zwischen Eltern- und Kindkomponente, keine allgemeine Umgehung des normalen Props-und-Events-Datenflusses. Wer defineExpose ausschließlich für einmalige Aktionen wie validate(), focus() oder open()/close() einsetzt und reaktive Daten weiterhin konsequent über Props und Events führt, behält Komponentenschnittstellen, die sowohl flexibel als auch nachvollziehbar bleiben.
| Anwendungsfall | Empfohlener Mechanismus | Warum | Beispiel |
|---|---|---|---|
| Formular extern validieren | defineExpose mit validate() | Einmalige, imperative Aktion vom Elternteil ausgelöst | formRef.value?.validate() |
| Modal öffnen/schließen | defineExpose mit open()/close() | Sichtbarkeitslogik bleibt intern gekapselt | modalRef.value?.open() |
| Auswahlwert an Elternteil melden | Props und Events (v-model) | Kontinuierlicher, reaktiver Datenfluss | @update:modelValue |
| Ladezustand an Elternteil melden | Props und Events | Reaktiver Zustand, der sich über Zeit ändert | emit('loading-change', true) |
Mironsoft
Vue-Architektur, Composition API und Nuxt-Performance
Vue-Anwendungen, die mit jedem Feature nicht komplizierter werden?
Wir prüfen bestehende Vue- und Nuxt-Projekte auf unstrukturierte Composables, ungenutzte Reaktivität und aufgeblähte Bundles und bauen daraus eine Architektur, die neue Features aufnimmt, ohne die Codebasis unübersichtlicher zu machen.
Architektur-Review
Composables, State-Management und Komponentenstruktur auf Wartbarkeit prüfen.
Performance-Audit
Reaktivitäts-Overhead, Bundle-Größe und Nuxt-Rendering-Strategie systematisch optimieren.
Nuxt-Integration
SSR/SSG-Setup und API-Anbindung robust und typsicher aufbauen.
10. Zusammenfassung
defineExpose und Template-Refs: Das Wichtigste auf einen Blick
Standardverhalten
script setup Komponenten sind ohne defineExpose vollständig gekapselt, ein Template-Ref liefert ein leeres Objekt.
defineExpose
Kompiler-Makro, das gezielt einzelne Methoden und Properties auf der public instance sichtbar macht.
Typisierung
InstanceType
Anti-Pattern
Reaktive Daten gehören zu Props und Events, defineExpose ist für einmalige, imperative Aktionen gedacht.