skip to content

In VeeValidate 4, why does a sign-up email field bound with defineField show an error on the first keystroke, and how do you make it validate on blur instead?

level: seniorimportance: should knowfreq 23%

answer

  1. aggressive by default
  2. the model setter validates
  3. four trigger switches
  4. per field, per function, global
  5. useField reads its own option

basics

~20 s

defineField's model validates on every update because validateOnModelUpdate defaults to true, and v-model updates on each keystroke. Pass defineField('email', { validateOnModelUpdate: false }) and the field validates on blur and change instead, which stay on by default.

solid answer

~40 s

`v-model` writes the model on every input event, and `defineField`'s model setter validates whenever `validateOnModelUpdate` is true, its default, so the first keystroke already produces an error. The other triggers default to `validateOnBlur: true`, `validateOnChange: true` and `validateOnInput: false`, so turning `validateOnInput` off changes nothing. Pass `{ validateOnModelUpdate: false }` to `defineField` for a lazy field, or a function of the field state for an eager one, such as validating on every update only once the field has errors. `configure()` sets the same defaults globally for `<Field>` and, in the source, for `defineField`; `useField` ignores them and uses its own `validateOnValueUpdate` option.

code

ts · 13 lines
ts
import { useForm } from 'vee-validate'

const { defineField } = useForm({ /* validationSchema */ })

// lazy: validate on blur and change only
const [email, emailAttrs] = defineField('email', {
  validateOnModelUpdate: false,
})

// eager: quiet until invalid, then validate on every update
const [password, passwordAttrs] = defineField('password', (state) => ({
  validateOnModelUpdate: state.errors.length > 0,
}))

go deeper

for a junior

Recall that defineField validates as you type by default and that validateOnModelUpdate: false makes it wait for blur.

for a middle

Explain the four trigger defaults, why validateOnInput is not the cause, and how a function config makes a field eager.

for a senior

Show you know configure() reaches Field and defineField but not useField, and how validated-only mode keeps a schema from flooding untouched fields.

for a principal

Set a house rule for validation timing across forms, applied through configure and shared field components, and document the useField exception.

## Where the keystroke validation comes from On a native text input, `v-model` listens to the `input` event, so the model is written on every keystroke. The model `defineField` returns is a writable computed whose setter writes the form value and asks for validation when **`validateOnModelUpdate`** is true. That is the default, which the docs call **aggressive** validation. So the error on the first keystroke is not the `input` trigger at all; it is the model-update trigger. ## The trigger settings and their defaults | Setting | Default | What fires it with `defineField` | |---|---|---| | `validateOnModelUpdate` | `true` | the model setter, via `v-model` | | `validateOnBlur` | `true` | the attrs' `onBlur` | | `validateOnChange` | `true` | the attrs' `onChange` (commit, not keystroke) | | `validateOnInput` | `false` | the attrs' `onInput` | | `bails` | `true` | not a trigger: stop at the first failing rule | Because `validateOnInput` is already `false`, setting it to `false` again changes nothing. ## Three places to configure timing 1. **Per field, static.** `defineField('email', { validateOnModelUpdate: false })` makes the field lazy: typing only updates the value, and validation runs on blur and change. 2. **Per field, dynamic.** Pass a function of the field state instead of an object. The docs' eager pattern is `state => ({ validateOnModelUpdate: state.errors.length > 0 })`: quiet until the first error, then re-validating on each keystroke so the error clears the moment the value is fixed. 3. **Globally.** `configure({ validateOnModelUpdate: false })` from `vee-validate` changes the app-wide defaults; call it once at startup, before forms mount. One detail differs between docs and code. The configuration page says the trigger options only apply to `<Field />`, not to `useField`. In the 4.15 source, `defineField` falls back to the global config for each trigger it was not given, so `configure()` affects `defineField` too. `useField` does not read those options: it uses its own `validateOnValueUpdate` option, default `true`, and its `handleBlur(evt, shouldValidate = false)` only marks the field touched unless you pass `true`. ## How a form schema interacts with timing With a form-level `validationSchema`, validating one field runs the schema over the whole form, batched in short windows. The results update every field's `valid` flag, but in the normal mode **error messages are only set for fields that have been validated at least once**. That is why typing in `email` does not flood `password` with messages. Submission validates every field, so all messages appear then. ## Components change which triggers exist The table assumes a native input that emits `input`, `change` and `blur`. A UI-kit component bound with `defineField` may emit only `update:modelValue`. Then: - the model-update trigger is the **only** one guaranteed to fire, which is why the docs describe component validation as immediate by default; - making such a field lazy with `validateOnModelUpdate: false` can leave it validating **only on submit**, unless the component also emits `blur` for the attrs' `onBlur` to catch; - the eager function config still works, because it only depends on model updates and the field's own error state. Check which events a component emits before choosing a lazy timing for it. ## Choosing timing for the sign-up form - **Email:** lazy or eager. Nobody benefits from 'invalid email' after the first character. - **Password:** eager works well; once the length error shows, it should disappear the moment the rule passes. - **Password confirmation:** validate on model update once touched, so a match is confirmed immediately. - **Showing messages:** gate them on the field's `touched` state as well, which the docs recommend so fields are not aggressive towards users. ## Mistakes worth catching - Setting `validateOnInput: false` to stop keystroke validation: it was never on. - Calling `configure()` and expecting a `useField`-based input component to change: that component needs its own `validateOnValueUpdate` option. - Leaving out `v-bind` of the attrs on a lazy field: without `onBlur`, a lazy field only validates on submit.

  • Why does configure({ validateOnModelUpdate: false }) not change a reusable input built with useField?
    `useField` has its own option, `validateOnValueUpdate`, which defaults to `true` and is not read from the global config. Pass `useField(() => props.name, undefined, { validateOnValueUpdate: false })` in that component, and call `handleBlur(evt, true)` if blur should validate.
  • What does bails: true change?
    With several rules on one field, validation stops at the first failing rule, so the field reports one message. `bails: false` runs every rule and collects all messages in `errorBag`. The docs note it does not affect yup schemas, which have their own abort behaviour.

saying these in an interview costs you the question

  • Setting validateOnInput to false stops validation on each keystroke.
  • The first-keystroke error means the schema is wrong.
  • configure() changes useField's validation timing as well.
  • With a form schema, typing in one field shows errors on every untouched field.
  • Lazy validation needs no v-bind, because v-model reports blur.