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.
Inhaltsverzeichnis
- 1. Warum Dirty State mehr ist als eine technische Details
- 2. Dirty State und Pristine State manuell implementieren
- 3. VeeValidate: Formular-State aus einer Hand
- 4. Zod-Schema für typsichere Validierung
- 5. Wann zeige ich Fehlermeldungen? Das Feedback-Timing
- 6. Asynchrone Validierung: API-Checks mit Debounce
- 7. Submit-Handling: pending, success und error
- 8. Cross-Field-Validierung und abhängige Felder
- 9. Vergleich der Validierungsansätze
- 10. Zusammenfassung
- 11. FAQ
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.