Lazy Validation in VeeValidate v4: validateOn, TypeScript, and Migration
Learn how to make VeeValidate v4 validation lazy with validateOn, keep TypeScript safety, and migrate cleanly from the v3 directive approach.
18 Nov 2025, 16:55 UTC

The problem: validation runs too early
When a form mounts, VeeValidate v4 validates every field by default. For large forms this means a cascade of rule evaluations before the user has typed anything, causing unnecessary re‑renders and noisy error messages. The fix is to make validation lazy — run rules only after the first interaction — and to keep the TypeScript safety net intact while moving away from the old v-validate directive.
Choosing a validation trigger with validateOn
The useField composable accepts a validateOn option that can be 'input', 'blur', 'change', or 'submit'. Setting it to 'change' (or 'blur') defers the first validation until the user leaves the field, which is the classic lazy pattern.
import { useField, useForm } from 'vee-validate';
import * as yup from 'yup';
const schema = yup.object({
email: yup.string().required().email(),
password: yup.string().required().min(8),
});
const { handleSubmit, resetForm } = useForm({ validationSchema: schema });
const email = useField('email', undefined, { validateOn: 'change' });
const password = useField('password', undefined, { validateOn: 'change' });
With this configuration the error ref for each field stays undefined until the user interacts, keeping the initial render clean.
TypeScript inference keeps errors typed
When useForm receives a Yup (or Zod) schema, VeeValidate infers the shape of errors and values. The rule names become literal keys, so a typo like emial is caught at compile time.
const { errors } = useForm({ validationSchema: schema });
// errors is typed as { email?: string; password?: string }
Pairing useField with the same schema gives you per‑field error refs that share the same type information, eliminating runtime surprises.
Migrating from the v3 directive
- Replace
v-validate="{ rules: ['required', 'email'] }"with a composable call:const { value, error } = useField('email'). - Bind the input with
v-model="value"and displayerror.valuewhere you previously usederrors.email. - Call
resetForm()(orfield.reset()) after a successful submit to clear both values and error state — something the old directive handled automatically.
The migration is mechanical, but you must remember that validateOn now defaults to 'input' (validate on mount) unless you override it, a behavioral shift from v3's interaction‑first approach.
Trade‑off: default validation on mount
If you omit validateOn, VeeValidate validates immediately on component creation. This can be useful for server‑side rendered forms that need to show pre‑existing errors, but for most SPAs it adds noise. The workaround is explicit: always set validateOn: 'change' (or 'blur') for lazy forms, or call resetForm() right after mount to clear the initial error state.
How to verify the behavior
- Open Vue DevTools, select a component using
useField, and watch theerrorref. It should stayundefineduntil you type and leave the field. - Run
npm list vee-validateto confirm you are on v4.x, then check the project's CHANGELOG for the exactvalidateOndefault in that release. - Create a minimal Vue 3 component with
<input v-model="val" @input="validate">and a required rule; observe whether the error appears before any keystroke.
Closing checklist
- Set
validateOn: 'change'(or'blur') on everyuseFieldthat should stay quiet on load. - Provide a Yup/Zod schema to
useFormfor full TypeScript inference. - Call
resetForm()after successful submissions to avoid stale errors. - Validate the lazy behavior with Vue DevTools or a quick mount test before shipping.
Following these steps gives you a form that feels responsive, stays type‑safe, and avoids the "validated on mount" surprise that trips up many migrations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.