skip to content

VeeValidate

VeeValidate validates Vue forms with rules or a schema and tracks errors per field and per form. Expect questions on binding it to component inputs and on where validation state lives.

on this pageshow

questions

5

With VeeValidate 4's useForm and defineField, how do you bind a sign-up form's email input and show its validation error?

level: juniorimportance: must knowfreq 34%

answer

  1. one form context per component
  2. a tuple from one call
  3. v-model plus v-bind
  4. errors keyed by path
  5. older binders are deprecated

basics

~10 s

Call useForm with a schema, then const [email, emailAttrs] = defineField('email'). Bind the input with v-model="email" and v-bind="emailAttrs", and render errors.email, the field's first error message.

solid answer

~40 s

`useForm()` turns the component into a form: it collects values, runs the `validationSchema` and aggregates errors and meta. `defineField('email')` returns a tuple: a writable model ref for `v-model`, and an attrs object with `onBlur`, `onChange` and `onInput` handlers for `v-bind`, which let VeeValidate mark the field touched and validate on blur. The first error for the field is `errors.email`; `errorBag.email` holds all of them. `defineField` is the current binding API in VeeValidate 4; `defineInputBinds`, `defineComponentBinds` and `useFieldModel` are deprecated in its favour. By default the model validates on every update, so the error appears while typing unless you configure the field otherwise.

code

vue · 26 lines
vue
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

const { defineField, errors, handleSubmit } = useForm({
  validationSchema: toTypedSchema(
    z.object({ email: z.string().email('Enter a valid email') }),
  ),
  initialValues: { email: '' },
})

const [email, emailAttrs] = defineField('email')

const onSubmit = handleSubmit((values) => {
  console.log(values.email)
})
</script>

<template>
  <form novalidate @submit="onSubmit">
    <input v-model="email" v-bind="emailAttrs" type="email" />
    <p v-if="errors.email">{{ errors.email }}</p>
    <button>Sign up</button>
  </form>
</template>

go deeper

for a junior

Recall the shape: useForm once, defineField per input returning a model and attrs, v-model plus v-bind, and errors.email for the message.

for a middle

Explain what the attrs carry and why blur matters, the difference between errors and errorBag, and what form meta aggregates.

for a senior

Show you know defineField replaced the deprecated binders, that it validates on every model update by default, and how to map field state into a UI kit's props.

for a principal

Discuss standardising one binding style across a codebase, so every form reads errors, touched state and submission the same way.

## Two ways into VeeValidate VeeValidate 4 offers the same engine through two surfaces. The **composition API** (`useForm`, `defineField`, `useField`, `useFieldArray`) is what the docs recommend for most work, because it fits any input element or UI library. The **component API** (`<Form>`, `<Field>`, `<ErrorMessage>`, `<FieldArray>`) wraps those same composables for template-first code. A form built with one can host fields built with the other. ## Declaring the form and binding a field `useForm()` creates a **form context** in the current component and provides it to children. It collects field values, validates them with the `validationSchema` you pass, and aggregates errors plus `touched`, `dirty`, `valid` and `pending` state. Call it once per component. ```ts // <script setup lang="ts"> of SignUpForm.vue import { useForm } from 'vee-validate' import { toTypedSchema } from '@vee-validate/zod' import { z } from 'zod' const { defineField, errors } = useForm({ validationSchema: toTypedSchema( z.object({ email: z.string().email('Enter a valid email') }), ), initialValues: { email: '' }, }) const [email, emailAttrs] = defineField('email') ``` In the template, `<input v-model="email" v-bind="emailAttrs" type="email" />` wires both halves, and `<p v-if="errors.email">{{ errors.email }}</p>` shows the message. ## What defineField returns | Part | What it is | How you use it | |---|---|---| | `email` | a writable ref whose setter writes the form value | `v-model` | | `emailAttrs` | a computed object with `onBlur`, `onChange`, `onInput` and any props you map | `v-bind` | - **The model** writes through the form, so `values.email` updates; you never assign to `values` directly, because the docs treat it as read-only. - **The attrs** are how blur reaches VeeValidate: `onBlur` sets the field's `touched` flag and validates when blur validation is on, which it is by default. - **Mapped props**: `defineField('email', { props: state => ({ 'aria-invalid': state.errors.length > 0 }) })` adds attributes computed from the field's state, useful for UI kits that take an `error` prop. - **The path** can be nested (`'profile.email'`) or indexed (`'phones[0]'`); values are nested accordingly. ## Reading errors and state - `errors` maps each field path to its **first** error message, or nothing when valid. - `errorBag` maps each path to the **array** of messages, for fields that list every failed rule. - `meta` is the form-level aggregate: `touched` (some field blurred), `dirty` (some value changed), `valid`, `pending` (a validation still running) and `initialValues`. - The keys of `errors` are flat path strings, so a nested field's error is read as `errors['profile.email']`. ## Native inputs versus components `defineField` binds both, but they behave differently by default: - **A native `<input>`** emits `input`, `change` and `blur`, so the attrs' handlers fire as expected and every trigger setting has an event behind it. - **A component**, such as a UI kit's text field, may not emit those events at all. The docs therefore describe component validation as running immediately, on model updates, because that is the one channel every `v-model` component shares. - **Mapping props** is how a component receives state: many kits accept an `error` or `invalid` prop, which you fill from the field state through the `props` option rather than rendering a separate message. If a component does emit `blur`, `v-bind` of the attrs forwards `onBlur` as a listener, and touched state and blur validation work the same as on a native input. ## The deprecated binders Earlier VeeValidate 4 releases bound fields with `defineInputBinds` for native inputs, `defineComponentBinds` for components and `useFieldModel` for a bare model. All three are marked `@deprecated use defineField instead` in 4.15. They still run, but new code and answers should use `defineField`, which covers both native inputs and components. ## Mistakes juniors make 1. **Only `v-model`, no `v-bind`.** The value flows, but blur never reaches the form, so `touched` stays false and blur validation never runs. 2. **No initial value.** Fields start as `undefined`; the docs recommend `initialValues` because some schema libraries treat `undefined` differently from an empty string. 3. **Writing to `values`.** Assigning `values.email = ''` bypasses the form's API; use `setFieldValue` or `resetForm`. 4. **Expecting silence while typing.** `defineField` validates on every model update by default, so a half-typed address already shows an error; making that lazier is a configuration choice, not a bug.

  • How would you show the email error only after the user has left the field?
    Read the field's touched state and gate the message on it. With `defineField` you can map it through the `props` option, for example `props: state => ({ showError: state.touched && state.errors.length > 0 })`, or read it with the `useIsFieldTouched('email')` helper. The attrs' `onBlur` is what sets `touched`, so the `v-bind` must be present.
  • When would you use useField instead of defineField?
    When building a reusable input component. `useField(() => props.name)` inside the component returns its own `value`, `errorMessage`, `meta` and handlers, and registers with the nearest form automatically. `defineField` is for binding fields from the component that owns the form.
  • What is the component-API equivalent of this form?
    `<Form :validation-schema="schema">` around `<Field name="email" type="email" />` and `<ErrorMessage name="email" />`. The components use the same composables internally, so the validation behaviour and the error keys are the same.

saying these in an interview costs you the question

  • defineField returns the field's value and its error message.
  • v-model alone is enough for VeeValidate to know when the field was blurred.
  • defineInputBinds is the recommended way to bind inputs in VeeValidate 4.15.
  • You reset the email by assigning values.email = ''.
  • errors.email holds an array of every failed rule's message.
open as a page

In VeeValidate 4 with a toTypedSchema Zod schema, why are form values typed partial while handleSubmit's callback gets required fields, and when does the callback not run?

level: middleimportance: should knowfreq 27%

basics

~20 s

toTypedSchema tells VeeValidate the schema's input type, a deep partial of what users are filling in, and its output type, what a valid parse returns. handleSubmit validates everything first and only calls your callback, with the output, when the form is valid.

open as a page

In a VeeValidate 4 sign-up form, how do you render a repeatable phone list with useFieldArray, and why key rows by field.key but name them by index?

level: seniorimportance: should knowfreq 19%

basics

~20 s

useFieldArray('phones') returns read-only fields plus helpers such as push and remove. Key the v-for on field.key, a stable generated id, but name each input phones[idx], because the form stores an array and paths are positional.

open as a page

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%

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.

open as a page

In a VeeValidate 4 sign-up form validated by one Zod schema, why does an async username-availability refine fire on keystrokes in other fields, and how do you contain it?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

With a form-level schema, validating any field parses the whole schema, so an async username refine runs whenever email or password validate. Cache the lookup per username, make the field lazy, and show meta.pending while it runs.

open as a page