<v/>
{ }
Vue 3 · Formular-Validierung · Dirty State · Async Checks
Forms in Vue 3:
Validation, Dirty State und Async Checks

Ein Formular, das dem Nutzer erst nach dem Absenden mitteilt, dass die E-Mail-Adresse bereits vergeben ist, schafft vermeidbare Frustration. Professionelle Formularimplementierung in Vue 3 bedeutet: Dirty State erkennen, Validierung zum richtigen Zeitpunkt zeigen und asynchrone Checks sauber mit Debounce und Ladezustand integrieren.

13 Min. Lesezeit VeeValidate · Zod · useDebounceFn · Dirty State Vue 3.4+ · @vee-validate/zod

1. Warum Dirty State mehr ist als ein technisches Detail

Der Dirty State eines Formularfelds gibt an, ob der Nutzer den ursprünglichen Wert geändert hat. Er ist kein akademisches Konzept, sondern hat direkten Einfluss auf die Nutzererfahrung: Ein Formular, das sofort nach dem Laden Fehlermeldungen für leere Pflichtfelder zeigt, wirkt aggressiv und desorientiert den Nutzer. Erst wenn ein Feld den Dirty State erreicht hat – also vom Nutzer tatsächlich berührt und verändert wurde – sollte die Validierungsmeldung erscheinen. Das Gegenteil des Dirty State ist der Pristine State: Das Feld hat seinen Initialwert, der Nutzer hat es noch nicht angefasst.

Neben dem reinen UX-Aspekt ist der Dirty State auch technisch wertvoll: Er ermöglicht es, "Haben Sie ungespeicherte Änderungen?"-Dialoge beim Verlassen der Seite nur dann anzuzeigen, wenn tatsächlich Änderungen vorliegen. Er hilft dabei, nur die geänderten Felder an die API zu schicken, statt das gesamte Formular. In Vue 3 muss man den Dirty State entweder manuell implementieren oder eine Bibliothek wie VeeValidate nutzen, die ihn mitliefert. Beide Ansätze haben ihre Daseinsberechtigung, und dieser Artikel zeigt beide im Praxiskontext.

2. Dirty State und Pristine State manuell implementieren

Eine manuelle Dirty State-Implementierung in Vue 3 basiert auf dem Vergleich zwischen dem aktuellen Feldwert und dem Initialwert. Man speichert den Initialwert in einer separaten Ref, die beim Mount gesetzt und beim Form-Reset aktualisiert wird. Der Dirty State ist dann ein computed(), der die aktuelle Ref mit dem gespeicherten Initialwert vergleicht. Für komplexe Objekte muss dieser Vergleich deep sein, was man mit JSON.stringify() oder einer Deep-Equal-Funktion löst.

Das Touched-State-Konzept ergänzt den Dirty State: Ein Feld ist "touched", wenn der Nutzer es fokussiert und wieder verlassen hat, unabhängig davon, ob er etwas geändert hat. VeeValidate nutzt beide Konzepte: meta.dirty für Wertänderungen und meta.touched für Blur-Ereignisse. Das erlaubt feingranulares Feedback-Timing: Fehlermeldungen nach dem Blur zeigen (touched), aber nur wenn der Nutzer tatsächlich etwas eingegeben hat (dirty). Diese Kombination bildet das Rückgrat jeder professionellen Vue 3 Formular-Validierung.


// composables/useFieldState.js — manual dirty and touched state tracking
import { ref, computed } from 'vue'

export function useFieldState(initialValue) {
  const value = ref(initialValue)
  const initialSnapshot = ref(initialValue)
  const touched = ref(false)

  // Dirty: current value differs from initial value
  const isDirty = computed(() => {
    return JSON.stringify(value.value) !== JSON.stringify(initialSnapshot.value)
  })

  // Pristine is the inverse of dirty
  const isPristine = computed(() => !isDirty.value)

  // Mark as touched when user leaves the field
  function onBlur() {
    touched.value = true
  }

  // Reset field to initial value and clear state
  function reset() {
    value.value = initialSnapshot.value
    touched.value = false
  }

  // Update initial snapshot (e.g. after successful save)
  function markAsSaved() {
    initialSnapshot.value = value.value
  }

  return { value, isDirty, isPristine, touched, onBlur, reset, markAsSaved }
}

3. VeeValidate: Formular-State aus einer Hand

VeeValidate ist die meistgenutzte Validierungsbibliothek für Vue 3 Formulare. Die Composition API von VeeValidate – useForm(), useField() und Form/Field-Komponenten – liefert Dirty State, Touched State, Validierungsstatus und Submit-Handling als vollständig reaktive Refs. Das meta-Objekt jedes Felds enthält dirty, touched, valid und pending (für laufende Async-Validierungen). useForm().meta aggregiert diese Zustände für das gesamte Formular.

Die Integration von VeeValidate in den Composition API-Stil von Vue 3 ist nahtlos. Man definiert das Formular mit useForm({ validationSchema }), definiert Felder mit useField('fieldName') und bindet value, errorMessage und handleChange/handleBlur direkt ans Template. VeeValidate kümmert sich automatisch um Dirty State-Tracking, Validierungszeitpunkt (on-change vs. on-blur) und den Submit-Lifecycle. Für neue Projekte in Vue 3 ist VeeValidate mit Zod die empfohlene Kombination für professionelle Formular-Validierung.

4. Zod-Schema für typsichere Validierung

Zod ist eine TypeScript-first Schema-Validierungsbibliothek, die sich ideal mit VeeValidate kombinieren lässt. Ein Zod-Schema definiert den Typ und die Validierungsregeln in einem einzigen, typsicheren Ausdruck. Das Schema dient gleichzeitig als TypeScript-Typ-Ableitung via z.infer<typeof schema> und als Laufzeit-Validator. Mit dem @vee-validate/zod-Adapter wird das Zod-Schema direkt als VeeValidate-Validierungsschema genutzt – ohne manuelle Konvertierung.

Der große Vorteil von Zod in Vue 3 Formularen: Das gleiche Schema kann auf dem Server zur API-Validierung verwendet werden (z.B. in einer Nuxt Server Route), sodass Frontend- und Backend-Validierungsregeln nie auseinanderlaufen. Wenn die E-Mail-Länge auf dem Server auf 254 Zeichen begrenzt ist, ist sie es dank des geteilten Zod-Schemas automatisch auch im Frontend. Dieses DRY-Prinzip bei der Formular-Validierung ist einer der stärksten Argumente für Zod im Vue 3-Ökosystem.


// composables/useRegistrationForm.js — VeeValidate with Zod schema
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

// Define schema once, use for both frontend and backend validation
const registrationSchema = z.object({
  email: z
    .string()
    .email('Ungültige E-Mail-Adresse.')
    .max(254, 'E-Mail-Adresse zu lang.'),
  password: z
    .string()
    .min(8, 'Mindestens 8 Zeichen erforderlich.')
    .regex(/[A-Z]/, 'Mindestens ein Großbuchstabe erforderlich.')
    .regex(/[0-9]/, 'Mindestens eine Zahl erforderlich.'),
  passwordConfirm: z.string(),
}).refine((data) => data.password === data.passwordConfirm, {
  message: 'Passwörter stimmen nicht überein.',
  path: ['passwordConfirm'],
})

export type RegistrationForm = z.infer<typeof registrationSchema>

export function useRegistrationForm() {
  const { handleSubmit, meta, setFieldError } = useForm({
    validationSchema: toTypedSchema(registrationSchema),
  })

  return { handleSubmit, meta, setFieldError }
}

5. Wann zeige ich Fehlermeldungen? Das Feedback-Timing

Das Timing von Fehlermeldungen ist eine der wichtigsten UX-Entscheidungen bei der Formular-Validierung in Vue 3. Zu früh angezeigte Fehler stören den Eingabefluss, zu spät angezeigte Fehler frustrieren beim Submit. Die bewährteste Strategie kombiniert mehrere Auslöser: Fehlermeldungen erscheinen erstmals, wenn der Nutzer das Feld verlässt (on-blur) und Dirty State vorliegt. Danach werden sie bei jeder weiteren Änderung sofort aktualisiert (on-change), damit der Nutzer direktes Feedback bekommt, ob seine Korrektur erfolgreich ist.

VeeValidate setzt dieses Muster standardmäßig um: Validierung läuft on-change und on-blur, aber Fehlermeldungen werden nur nach dem ersten Blur-Event oder nach dem Submit-Versuch angezeigt. Dieses Verhalten lässt sich über den validateOnMount-, validateOnChange- und validateOnBlur-Optionen anpassen. Für Felder, die häufig korrekt ausgefüllt werden (z.B. Vorname), ist later-validation (erst on-blur) besser. Für kritische Felder wie Passwörter mit Sicherheitsanforderungen ist early-validation (on-change, sobald dirty) nützlicher, weil der Nutzer sofort sieht, welche Anforderungen er bereits erfüllt.

6. Asynchrone Validierung: API-Checks mit Debounce

Asynchrone Validierungen – der häufigste Anwendungsfall ist die Prüfung, ob eine E-Mail-Adresse bereits registriert ist – sind die komplexeste Variante der Formular-Validierung in Vue 3. Ohne Debounce würde bei jedem Tastendruck eine API-Anfrage ausgelöst, was die API belastet und dem Nutzer unvollendete Eingaben validiert. Mit useDebounceFn() aus VueUse wartet man eine definierte Zeit nach dem letzten Tastendruck ab, bevor die Anfrage gesendet wird. 400–600 ms sind für Async-Validierungen ein guter Startwert.

Während der asynchrone Check läuft, zeigt man dem Nutzer einen Ladezustand – entweder ein Spinner im Eingabefeld oder eine neutrale Statusmeldung wie "Wird geprüft...". VeeValidate setzt meta.pending automatisch auf true, während ein Async-Validator läuft. In Zod lassen sich Async-Validierungen über z.string().refine(async (email) => { ... }) einbinden. Ein wichtiges Detail: Async-Validierungen sollten abgebrochen werden, wenn der Nutzer das Feld verlässt oder ein neuer Check startet – mit AbortController lassen sich laufende Fetch-Requests zuverlässig abbrechen.


// composables/useEmailValidation.js — async email uniqueness check with debounce
import { ref } from 'vue'
import { useDebounceFn } from '@vueuse/core'

export function useEmailUniqueCheck() {
  const isChecking = ref(false)
  const lastCheckedEmail = ref('')
  let abortController = null

  const checkEmailUnique = useDebounceFn(async (email, setError, clearError) => {
    // Skip check if email hasn't changed or is invalid format
    if (email === lastCheckedEmail.value || !email.includes('@')) return

    // Abort previous in-flight request
    if (abortController) abortController.abort()
    abortController = new AbortController()

    isChecking.value = true
    lastCheckedEmail.value = email

    try {
      const response = await fetch(`/api/auth/check-email?email=${encodeURIComponent(email)}`, {
        signal: abortController.signal,
      })
      const { available } = await response.json()

      if (!available) {
        setError('email', 'Diese E-Mail-Adresse ist bereits registriert.')
      } else {
        clearError('email')
      }
    } catch (err) {
      if (err.name !== 'AbortError') {
        // Network error — do not block form submission
        console.warn('[EmailCheck] API nicht erreichbar:', err.message)
      }
    } finally {
      isChecking.value = false
    }
  }, 500)

  return { checkEmailUnique, isChecking }
}

7. Submit-Handling: pending, success und error

Das Submit-Handling eines Vue 3 Formulars muss drei Zustände sauber verwalten: den laufenden Submit (pending), den Erfolgsfall (success) und den Fehlerfall (error), inklusive Server-seitiger Validierungsfehler, die als Feldfehler zurückgespielt werden müssen. Der handleSubmit()-Wrapper von VeeValidate stellt sicher, dass das Formular vollständig valide ist, bevor der Submit-Callback aufgerufen wird. Ohne diese Absicherung muss man die Validierung manuell triggern und prüfen.

Server-seitige Validierungsfehler – etwa "Diese Kombination aus Name und Firmenname existiert bereits" – sind Fehler, die nur der Server erkennen kann und die nach dem Submit als Feldfehler zurückgespielt werden müssen. VeeValidate's setFieldError() setzt solche externen Fehler direkt auf einzelne Felder. Wichtig: Nach einem erfolgreichen Submit sollte der Dirty State zurückgesetzt werden (resetForm()), damit der "Haben Sie ungespeicherte Änderungen?"-Guard beim nächsten Navigieren nicht unnötig feuert. Dieses Detail vergessen viele Implementierungen und verwirrt Nutzer.

8. Cross-Field-Validierung und abhängige Felder

Cross-Field-Validierung – Regeln, die mehrere Felder gleichzeitig betreffen – ist eine der anspruchsvollsten Anforderungen bei der Formular-Validierung in Vue 3. Das klassische Beispiel ist "Passwort" und "Passwort bestätigen", die übereinstimmen müssen. Zod löst das mit .refine() auf Objektebene, das Zugriff auf alle Feldwerte hat. In VeeValidate lässt sich Cross-Field-Validierung über den Zod-Adapter automatisch nutzen – der Fehler wird korrekt auf das passwordConfirm-Feld gemapped, nicht auf das Gesamtformular.

Eine weitere häufige Cross-Field-Anforderung: Datumsbereichsvalidierung, bei der startDate vor endDate liegen muss. Das superRefine()-Pendant von Zod mit mehreren Issues erlaubt es, gleichzeitig Fehler auf mehrere Felder zu setzen. In Formularen ohne Zod kann man Cross-Field-Validierung als Watch-Effekt implementieren: Wenn sich eines der abhängigen Felder ändert, wird die Validierung des anderen Felds neu ausgeführt. Dieses Pattern funktioniert zuverlässig, erfordert aber mehr manuelle Verdrahtung als der Zod-Ansatz.

9. Vergleich der Validierungsansätze in Vue 3

Die Wahl des richtigen Ansatzes für Formular-Validierung in Vue 3 hängt von den Anforderungen an Typsicherheit, Komplexität und Abhängigkeiten ab. Hier ein direkter Vergleich der gängigsten Strategien:

Ansatz Dirty State Async Support Typsicherheit
Manuell (ref + watch) Selbst implementieren Manuell mit Debounce Keine
VeeValidate allein meta.dirty eingebaut meta.pending eingebaut Eingeschränkt
VeeValidate + Zod meta.dirty eingebaut Zod async refine Vollständig (z.infer)
Formkit Eingebaut Eingebaut Eingeschränkt
Reaktives Objekt + native HTML5 Nicht vorhanden Manuell Keine

Für neue Vue 3-Projekte mit TypeScript ist VeeValidate + Zod die klare Empfehlung. Der Overhead der Bibliotheken ist minimal (VeeValidate ~7kB, Zod ~12kB gzipped), der Gewinn an Typsicherheit, Dirty State-Tracking und Async-Validierungssupport ist erheblich. Für kleine Formulare mit 2–3 Feldern ohne Async-Checks kann die manuelle Implementierung mit ref und watch ausreichen und Abhängigkeiten sparen.

Mironsoft

Vue 3-Formularentwicklung mit VeeValidate, Zod und optimaler UX

Formulare, die Nutzer nicht frustrieren?

Wir entwickeln Vue 3-Formulare mit professionellem Dirty State-Tracking, typsicherer Validierung via Zod, asynchronen API-Checks und dem richtigen Feedback-Timing – für Nutzererfahrungen, die sich intuitiv anfühlen.

Formular-Audit

Bestehende Formulare auf Dirty State, Validierungstiming und Async-Checks analysieren

Zod-Schema

Geteilte Validierungsschemas für Frontend und Backend – keine doppelte Pflege mehr

Async-Validierung

Debounced API-Checks mit Ladezustand, AbortController und sauberem Fehlerhandling

10. Zusammenfassung

Professionelle Formular-Validierung in Vue 3 umfasst mehr als Pflichtfeld-Prüfungen. Dirty State und Touched State steuern, wann Fehlermeldungen erscheinen, und verhindern aggressive frühe Validierung. VeeValidate liefert diese Zustände fertig aus der Box und integriert sich über den Zod-Adapter nahtlos mit typsicheren Schemas. Zod-Schemas lassen sich zwischen Frontend und Backend teilen, sodass Validierungsregeln nie auseinanderlaufen. Asynchrone Checks mit useDebounceFn und AbortController prüfen API-Constraints effizient, ohne die API zu überlasten.

Das Feedback-Timing – on-blur für die erste Anzeige, on-change für Updates – ist keine Kleinigkeit, sondern der Unterschied zwischen einem Formular, das sich intuitiv anfühlt, und einem, das Nutzer frustriert. Submit-Handling mit Server-Fehlerrückspielung via setFieldError() und Reset des Dirty State nach erfolgreichem Submit runden das Bild ab. Diese Kombination macht jede Vue 3 Formular-Validierung produktionsreif.

Vue 3 Forms — Das Wichtigste auf einen Blick

Dirty State

Aktueller Wert ≠ Initialwert. VeeValidate liefert meta.dirty und meta.touched fertig. Steuert, wann Fehlermeldungen erscheinen.

Zod-Integration

Zod-Schema via toTypedSchema() in VeeValidate einbinden. Selbes Schema für Frontend und Backend-Validierung – kein DRY-Verstoß.

Async-Checks

useDebounceFn() für 400–600ms Verzögerung. AbortController bricht laufende Requests ab. meta.pending zeigt Ladezustand im Template.

Submit-Lifecycle

handleSubmit() prüft vor Submit. Server-Fehler via setFieldError(). Nach Erfolg resetForm() für sauberen Dirty State.

11. FAQ: Forms in Vue 3 — Validation, Dirty State und Async Checks

1Was ist Dirty State in Vue 3?
Aktueller Wert ≠ Initialwert. VeeValidate stellt meta.dirty bereit. Steuert, wann Fehlermeldungen erstmals erscheinen dürfen.
2Dirty vs. Touched?
Dirty = Wert geändert. Touched = Feld fokussiert und verlassen (blur). Beide zusammen steuern das Feedback-Timing.
3VeeValidate + Zod vs. VeeValidate allein?
Zod liefert Typsicherheit mit z.infer und kann zwischen Frontend und Backend geteilt werden. VeeValidate allein hat keine vollständige TypeScript-Integration.
4Asynchrone Validierung in Vue 3?
useDebounceFn() (400–600ms) + AbortController für laufende Requests + meta.pending für Ladezustand im Template.
5Wann Fehlermeldung zeigen?
Erstmals nach erstem Blur (touched=true). Danach on-change für sofortige Updates. VeeValidate-Standard-Verhalten verhindert aggressive frühe Validierung.
6Server-Fehler im Formular anzeigen?
setFieldError('fieldName', 'Meldung') setzt Server-Fehler direkt auf das entsprechende Feld – erscheint wie ein lokaler Validierungsfehler.
7Cross-Field-Validierung?
Zod .refine() auf Objektebene mit Zugriff auf alle Felder. Fehler wird via Zod-Adapter automatisch auf das richtige Feld in VeeValidate gemapped.
8Dirty State nach Submit zurücksetzen?
resetForm() von VeeValidate nach erfolgreichem Submit. Setzt alle Felder zurück und cleariert dirty, touched und Fehler.
9Zod-Schema teilen zwischen Frontend und Backend?
Schema in shared/schemas.ts auslagern, in Vue-Frontend und Nuxt Server Routes gleichermaßen importieren. Keine doppelte Pflege.
10Zu viele API-Anfragen bei Async-Checks vermeiden?
useDebounceFn() mit 400–600ms + AbortController + nur prüfen wenn Formatierung bereits korrekt. Reduziert API-Last erheblich.