defineExpose und Template-Refs auf Kindkomponenten in Vue richtig einsetzen
AI generated
{ }
Vue.js · defineExpose · Template Refs · script setup
defineExpose und Template-Refs
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.

14 Min. Lesezeit defineExpose · Template Refs Vue 3 · script setup · Composition API

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 | null>(null) deklariert. InstanceType leitet dabei automatisch den Typ der öffentlichen Instanz aus der importierten Komponente ab, inklusive aller Properties und Methoden, die diese Komponente per defineExpose freigegeben hat, sofern die Kindkomponente selbst mit TypeScript geschrieben ist.

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 an, der spezifisch für den per defineExpose freigegebenen Typ ausgelegt ist und in manchen Randfällen genauere Ergebnisse liefert als InstanceType. Für die meisten Projekte reicht InstanceType jedoch vollständig aus und ist der etabliertere, ohne Zusatzabhängigkeit funktionierende Standardansatz.

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 leitet den korrekten Typ der exponierten Schnittstelle für den Elternteil-Ref ab.

Anti-Pattern

Reaktive Daten gehören zu Props und Events, defineExpose ist für einmalige, imperative Aktionen gedacht.

11. FAQ: defineExpose und Template-Refs: Das Wichtigste auf einen Blick

1Warum liefert ein Template-Ref auf eine script setup Komponente standardmäßig ein leeres Objekt?
Weil script setup Komponenten standardmäßig vollständig gekapselt sind. Ohne defineExpose ist die public instance der Komponente absichtlich leer, damit interne Implementierungsdetails nicht ungewollt von außen zugreifbar werden.
2Wie gibt man einzelne Methoden gezielt frei?
Mit dem Compiler-Makro defineExpose, aufgerufen mit einem Objekt aus den gewünschten Properties und Methoden, zum Beispiel defineExpose({ validate, reset }). Nur diese werden anschließend über einen Template-Ref vom Elternteil aus erreichbar.
3Wie greift der Elternteil auf die exponierten Methoden zu?
Über einen normalen Template-Ref auf die Kindkomponente. Sobald die Komponente gemountet ist, liefert der Ref genau das Objekt zurück, das per defineExpose freigegeben wurde, sodass etwa formRef.value?.validate() aufgerufen werden kann.
4Wie typisiert man einen Ref auf eine Kindkomponente korrekt?
Mit InstanceType, zum Beispiel ref | null>(null). Das leitet automatisch den Typ der öffentlichen Instanz inklusive der per defineExpose freigegebenen Methoden ab.
5Wann ist defineExpose ein Anti-Pattern?
Wenn es genutzt wird, um kontinuierlichen, reaktiven Datenfluss zu ersetzen, der eigentlich über Props und Events laufen sollte. Wird reaktiver Zustand nur per Ref abgefragt statt per Event emittiert, entsteht eine verdeckte, schwer nachvollziehbare Abhängigkeit.
6Existiert ein Äquivalent zu defineExpose in der Options API?
Ja, die expose-Option, angegeben als Array von Namen, etwa expose: ['validate', 'reset']. Das Verhalten ist konzeptuell identisch, nur die Syntax und der Deklarationsstil unterscheiden sich.
7Was passiert, wenn man auf formRef.value zugreift, bevor die Kindkomponente gemountet ist?
Der Ref ist dann noch null, ein Zugriff auf eine Methode führt entweder zu einem Fehler oder muss per optional chaining wie formRef.value?.validate() abgesichert werden, um saubere Fehlerbehandlung zu ermöglichen.
8Kann man mit defineExpose auch reaktive Refs freigeben, nicht nur Funktionen?
Ja, technisch können auch reaktive Refs freigegeben werden. Sinnvoll ist das aber selten, weil die reaktive Verbindung dann nicht automatisch, sondern nur bei aktiver Abfrage durch den Elternteil sichtbar wird, was der deklarativen Vue-Philosophie widerspricht.
9Muss defineExpose immer aufgerufen werden, auch ohne etwas freizugeben?
Nein, wird defineExpose gar nicht aufgerufen, bleibt die Komponente einfach vollständig gekapselt, was für die meisten Komponenten das gewünschte Standardverhalten ist und keinerlei zusätzlichen Code erfordert.
10Eignet sich defineExpose für Bibliotheks-Komponenten, die von externen Projekten genutzt werden?
Ja, gerade dort ist es sinnvoll, weil es explizit die öffentliche, stabile Schnittstelle einer Komponente definiert, während interne Implementierungsdetails geschützt bleiben und sich ändern lassen, ohne die öffentliche API zu brechen.