git:20260202.b236384 to git:20260316.00cb75b

81 added, 288 removed. Audit B to B.

---
name: web-forms-vee-validate
description: VeeValidate v4 patterns - useForm, useField, defineField, useFieldArray, schema validation with Composition API
---
# VeeValidate Form Validation Patterns
- > **Quick Guide:** Use VeeValidate v4 for Vue 3 form validation with Composition API. Use `useForm` for form state, `defineField` for quick field setup, `useField` for custom input components, and `useFieldArray` for dynamic lists.
+ > **Quick Guide:** Use VeeValidate v4 for Vue 3 form validation with Composition API. Use `useForm` for form state, `defineField` for quick field setup, `useField` for custom input components, and `useFieldArray` for dynamic lists. Always wrap schema libraries with `toTypedSchema()`. Always use `field.key` (not index) as iteration key in field arrays.
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST use `toTypedSchema()` wrapper when using schema libraries in v4 - raw schemas won't work)**
**(You MUST use `field.key` as iteration key in useFieldArray - NEVER use array index)**
**(You MUST use function form `() => props.name` or `toRef()` in useField for prop reactivity)**
**(You MUST initialize field array values in `initialValues` - undefined arrays cause errors)**
</critical_requirements>
---
**Auto-detection:** VeeValidate, vee-validate, useForm, useField, defineField, useFieldArray, toTypedSchema, ErrorMessage, Form component
**When to use:**
- Building Vue 3 forms with validation requirements
- Managing complex form state with multiple fields
- Creating dynamic forms with add/remove field capabilities
- - Integrating schema validation libraries (Zod, Yup, Valibot)
+ - Integrating schema validation libraries with `toTypedSchema()`
- Building multi-step wizard forms
- **Key patterns covered:**
-
- - useForm hook with TypeScript generics
- - defineField for quick native input binding
- - useField for custom input components
- - useFieldArray for dynamic field lists
- - Schema validation with toTypedSchema
- - Error handling and display
- - Form meta state tracking
-
**When NOT to use:**
- Single input without validation (use native v-model)
- Server-only forms with server actions (use native form submission)
- Read-only data display (not a form scenario)
**Detailed Resources:**
- - For code examples, see [examples/](examples/) folder:
- - [core.md](examples/core.md) - Basic form patterns
- - [validation.md](examples/validation.md) - Schema validation integration
- - [arrays.md](examples/arrays.md) - useFieldArray for dynamic forms
- - For decision frameworks and anti-patterns, see [reference.md](reference.md)
+ - [examples/core.md](examples/core.md) - defineField, useField, form meta, eager validation
+ - [examples/validation.md](examples/validation.md) - Zod/Yup/Valibot schema integration, conditional validation
+ - [examples/arrays.md](examples/arrays.md) - useFieldArray, nested arrays, reordering
+ - [reference.md](reference.md) - Decision frameworks, API reference tables, anti-patterns
---
<philosophy>
## Philosophy
VeeValidate v4 embraces Vue 3's Composition API as the primary approach, enabling seamless integration with any UI library. Validation logic is decoupled from presentation, allowing schema-first validation with full TypeScript inference.
**Core Principles:**
1. **Composition API first** - Use `useForm`, `useField`, `defineField` for seamless Vue 3 integration
- 2. **Schema-first validation** - Prefer declarative schemas (Zod/Yup) over inline rules
+ 2. **Schema-first validation** - Prefer declarative schemas over inline rules
3. **Full type safety** - TypeScript inference from schemas and generics
4. **UI agnostic** - Works with any component library or native inputs
5. **Minimal re-renders** - Efficient reactivity through Vue's reactive system
**defineField vs useField:**
| Feature | `defineField` | `useField` |
| ---------------- | ----------------------------------- | ----------------------------------------- |
| **Use case** | Quick form setup with native inputs | Building reusable custom input components |
| **Form context** | Always requires form context | Optional form integration |
| **Best for** | Application-level forms | Component library development |
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Basic Form with defineField
- Use `useForm` with `defineField` for the fastest form setup with native inputs.
+ Use `useForm` with `defineField` for the fastest form setup. `defineField` returns a `[model, attrs]` tuple for v-model binding. See [examples/core.md](examples/core.md) for full examples.
```vue
<script setup lang="ts">
import { useForm } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import { z } from "zod";
- const MIN_PASSWORD_LENGTH = 8;
-
- // Define schema with type inference
const schema = toTypedSchema(
z.object({
- email: z.string().min(1, "Email is required").email("Invalid email"),
- password: z
- .string()
- .min(MIN_PASSWORD_LENGTH, `At least ${MIN_PASSWORD_LENGTH} characters`),
+ email: z.string().email("Invalid email"),
+ password: z.string().min(8, "At least 8 characters"),
}),
);
- // Initialize form with typed schema
const { handleSubmit, errors, defineField } = useForm({
validationSchema: schema,
});
- // defineField returns [model, attrs] tuple
const [email, emailAttrs] = defineField("email");
- const [password, passwordAttrs] = defineField("password");
- // Type-safe submit handler
const onSubmit = handleSubmit((values) => {
- // values is fully typed: { email: string; password: string }
- console.log("Submitting:", values);
+ // values is fully typed from schema
});
</script>
-
- <template>
- <form @submit="onSubmit">
- <div>
- <label for="email">Email</label>
- <input id="email" v-model="email" v-bind="emailAttrs" type="email" />
- <span v-if="errors.email" role="alert">{{ errors.email }}</span>
- </div>
-
- <div>
- <label for="password">Password</label>
- <input
- id="password"
- v-model="password"
- v-bind="passwordAttrs"
- type="password"
- />
- <span v-if="errors.password" role="alert">{{ errors.password }}</span>
- </div>
-
- <button type="submit">Submit</button>
- </form>
- </template>
```
- **Why good:** toTypedSchema enables full type inference from Zod schema, defineField returns reactive model and attributes for v-model binding, errors object provides field-level error messages, named constant for MIN_PASSWORD_LENGTH
-
---
- ### Pattern 2: Typed Forms with Generics
+ ### Pattern 2: Custom Input Components with useField
- For explicit type control without schema libraries.
+ Use `useField` when building reusable input components. **Critical:** use function form `() => props.name` to maintain reactivity. See [examples/core.md](examples/core.md) for full component example.
```vue
<script setup lang="ts">
- import { useForm } from "vee-validate";
-
- interface LoginForm {
- email: string;
- password: string;
- rememberMe: boolean;
- }
-
- const { handleSubmit, errors, defineField } = useForm<LoginForm>({
- initialValues: {
- email: "",
- password: "",
- rememberMe: false,
- },
- });
+ import { useField } from "vee-validate";
- // Fields are typed based on LoginForm interface
- const [email, emailAttrs] = defineField("email");
- const [password, passwordAttrs] = defineField("password");
- const [rememberMe, rememberMeAttrs] = defineField("rememberMe");
+ const props = defineProps<{ name: string }>();
- const onSubmit = handleSubmit(async (values) => {
- // values: LoginForm
- await loginUser(values);
- });
+ // CRITICAL: Function form maintains reactivity
+ const { value, errorMessage, handleBlur, meta } = useField<string>(
+ () => props.name,
+ undefined,
+ { validateOnValueUpdate: false },
+ );
</script>
```
- **Why good:** TypeScript generics provide autocomplete and type checking for field names, initialValues establishes default state, explicit interface documents form shape
-
---
- ### Pattern 3: Custom Input Components with useField
-
- Use `useField` when building reusable input components.
-
- ```vue
- <!-- components/text-input.vue -->
- <script setup lang="ts">
- import { useField } from "vee-validate";
+ ### Pattern 3: Schema Validation with toTypedSchema
- interface Props {
- name: string;
- label: string;
- type?: string;
- placeholder?: string;
- }
+ Always wrap schema libraries with `toTypedSchema()`. Initialize ALL fields used in `refine/superRefine` - Zod skips refinements when keys are undefined. See [examples/validation.md](examples/validation.md) for Zod, Yup, and Valibot examples.
- const props = withDefaults(defineProps<Props>(), {
- type: "text",
- placeholder: "",
- });
+ ```typescript
+ import { toTypedSchema } from "@vee-validate/zod";
- // CRITICAL: Use function form to maintain reactivity
- const { value, errorMessage, handleBlur, handleChange, meta } =
- useField<string>(
- () => props.name, // Function form maintains reactivity
- undefined,
- {
- validateOnValueUpdate: false, // Lazy validation
- },
- );
- </script>
+ // CORRECT: Wrapped schema
+ const schema = toTypedSchema(z.object({ email: z.string().email() }));
- <template>
- <div class="form-field">
- <label :for="name">{{ label }}</label>
- <input
- :id="name"
- :name="name"
- :type="type"
- :value="value"
- :placeholder="placeholder"
- :class="{ 'has-error': meta.touched && errorMessage }"
- :aria-invalid="meta.touched && !!errorMessage"
- @input="handleChange"
- @blur="handleBlur"
- />
- <span v-if="meta.touched && errorMessage" role="alert">
- {{ errorMessage }}
- </span>
- </div>
- </template>
+ // WRONG: Raw schema won't work with VeeValidate
+ const schema = z.object({ email: z.string().email() });
```
- **Why good:** Function form `() => props.name` maintains reactivity when prop changes, validateOnValueUpdate: false enables lazy validation, meta.touched shows errors only after interaction, aria-invalid improves accessibility
-
---
- ### Pattern 4: Form Meta and State
+ ### Pattern 4: Dynamic Arrays with useFieldArray
- Access aggregated form state for UX features.
+ Use `useFieldArray` for add/remove/reorder patterns. **Always** use `field.key` as `:key`, never array index. Initialize arrays in `initialValues`. See [examples/arrays.md](examples/arrays.md) for full patterns.
```vue
<script setup lang="ts">
- import { useForm } from "vee-validate";
- import { toTypedSchema } from "@vee-validate/zod";
- import { z } from "zod";
-
- const schema = toTypedSchema(
- z.object({
- email: z.string().email(),
- }),
- );
+ import { useForm, useFieldArray } from "vee-validate";
- const { handleSubmit, meta, isSubmitting, resetForm, defineField } = useForm({
- validationSchema: schema,
- initialValues: {
- email: "",
- },
+ const { handleSubmit } = useForm({
+ initialValues: { users: [{ name: "", email: "" }] },
});
- const [email, emailAttrs] = defineField("email");
-
- const onSubmit = handleSubmit(async (values) => {
- await submitForm(values);
- });
+ const { fields, push, remove } = useFieldArray("users");
</script>
<template>
- <form @submit="onSubmit">
- <input v-model="email" v-bind="emailAttrs" type="email" />
-
- <button type="submit" :disabled="!meta.valid || isSubmitting">
- {{ isSubmitting ? "Submitting..." : "Submit" }}
- </button>
-
- <button type="button" @click="resetForm()">Reset</button>
-
- <p v-if="meta.dirty">You have unsaved changes</p>
- </form>
+ <!-- CORRECT: field.key as key -->
+ <div v-for="(field, index) in fields" :key="field.key">
+ <input v-model="field.value.name" />
+ </div>
</template>
```
- **Why good:** meta.valid enables submit button state, meta.dirty tracks unsaved changes, isSubmitting provides loading state, resetForm clears form to initialValues
-
---
- ### Pattern 5: Lazy Validation Control
-
- Control when validation triggers per field.
-
- ```vue
- <script setup lang="ts">
- import { useForm } from "vee-validate";
-
- const { defineField } = useForm({
- initialValues: { email: "", username: "" },
- });
-
- // Aggressive validation (default) - validates on every change
- const [email, emailAttrs] = defineField("email");
-
- // Lazy validation - validates on blur only
- const [username, usernameAttrs] = defineField("username", {
- validateOnModelUpdate: false, // Don't validate on input
- });
- </script>
- ```
-
- **Why good:** validateOnModelUpdate: false reduces validation noise during typing, per-field control enables different UX patterns
-
- ---
-
- ### Pattern 6: Server-Side Error Handling
-
- Set errors from API responses.
-
- ```vue
- <script setup lang="ts">
- import { useForm } from "vee-validate";
-
- interface ApiError {
- field: string;
- message: string;
- }
+ ### Pattern 5: Server-Side Error Handling
- const { handleSubmit, setErrors, setFieldError, errors, defineField } = useForm(
- {
- initialValues: { email: "", username: "" },
- },
- );
+ Set errors from API responses using `setErrors` (multiple) or `setFieldError` (single).
- const [email] = defineField("email");
- const [username] = defineField("username");
+ ```typescript
+ const { handleSubmit, setErrors, setFieldError } = useForm({ ... });
const onSubmit = handleSubmit(async (values) => {
try {
await api.createUser(values);
} catch (error) {
if (error.response?.data?.errors) {
// Set multiple field errors from API
- const apiErrors = error.response.data.errors as ApiError[];
- const errorMap = apiErrors.reduce(
- (acc, err) => {
- acc[err.field] = err.message;
- return acc;
- },
- {} as Record<string, string>,
- );
- setErrors(errorMap);
+ setErrors(mapApiErrors(error.response.data.errors));
} else {
- // Set single field error
setFieldError("apiError", "Something went wrong");
}
}
});
- </script>
-
- <template>
- <form @submit="onSubmit">
- <input v-model="email" type="email" />
- <span v-if="errors.email">{{ errors.email }}</span>
-
- <input v-model="username" />
- <span v-if="errors.username">{{ errors.username }}</span>
-
- <span v-if="errors.apiError">{{ errors.apiError }}</span>
-
- <button type="submit">Register</button>
- </form>
- </template>
```
- **Why good:** setErrors handles multiple API validation errors at once, setFieldError sets individual field errors, error state integrates seamlessly with form display
-
- </patterns>
-
---
- <decision_framework>
+ ### Pattern 6: Form Meta and State
- ## Decision Framework
+ Access aggregated form state for UX features like dirty tracking, submit button state, and reset. See [examples/core.md](examples/core.md) for full example.
- ### When to Use defineField vs useField
+ ```vue
+ <script setup lang="ts">
+ const { handleSubmit, meta, isSubmitting, resetForm } = useForm({ ... });
+ </script>
- ```
- Are you building the form directly in your component?
- ├─ YES → Are you using native HTML inputs?
- │ ├─ YES → Use defineField ✓
- │ └─ NO → Does your component accept v-model?
- │ ├─ YES → Use defineField with v-model ✓
- │ └─ NO → Use useField with manual binding
- └─ NO → Are you building a reusable input component?
- ├─ YES → Use useField ✓
- └─ NO → Use defineField for application forms
+ <template>
+ <button :disabled="!meta.valid || !meta.dirty || isSubmitting">
+ {{ isSubmitting ? "Saving..." : "Save" }}
+ </button>
+ <p v-if="meta.dirty">You have unsaved changes</p>
+ </template>
```
- ### Validation Mode Selection
-
- ```
- What validation UX do you need?
- ├─ Validate after first error then aggressively → default behavior ✓
- ├─ Validate only on blur → defineField with validateOnModelUpdate: false
- ├─ Validate only on submit → useForm with validateOnMount: false
- └─ Eager validation (lazy first, aggressive after error) → custom listeners
- ```
+ </patterns>
- ### Schema Library Selection
+ ---
- ```
- Which schema library should you use?
- ├─ Need smallest bundle size? → Valibot
- ├─ Already using Zod elsewhere? → Zod ✓ (recommended)
- ├─ Legacy codebase with Yup? → Yup
- └─ Functional validation style? → Valibot
- ```
+ <red_flags>
- </decision_framework>
+ ## RED FLAGS
- ---
+ **High Priority Issues:**
- <integration>
+ - Missing `toTypedSchema()` wrapper - raw schemas silently fail to validate
+ - Using array index as `:key` in `useFieldArray` - causes form state corruption on add/remove
+ - Direct `props.name` in `useField` - loses reactivity when prop changes
+ - Undefined `initialValues` for field arrays - causes runtime errors
- ## Integration Guide
+ **Medium Priority Issues:**
- **Styling Integration:**
- Components accept class bindings for styling flexibility.
- Use `:class="{ 'has-error': errorMessage }"` for error states.
- Validation state is exposed via `meta` object.
+ - `validateOnValueUpdate` enabled everywhere - validates on every keystroke (noisy UX)
+ - Not handling async validation errors - API failures need `setErrors()` or `setFieldError()`
+ - Forgetting `resetForm()` after submission - form stays dirty after success
+ - Multiple `useForm` calls in same component - creates conflicting form contexts
+ - Not using `meta.touched` for error display - shows errors before user interaction
- **State Integration:**
- Form values are managed internally by VeeValidate.
- Use `values` from useForm for reading current state.
- Server state synchronization handled via `resetForm(newValues)`.
+ **Gotchas & Edge Cases:**
- **Testing Integration:**
- Forms can be tested by triggering input events and submit.
- Mock validation schemas for isolated component tests.
- Use `flushPromises()` for async validation timing.
+ - `errors` has first error per field; `errorBag` has ALL errors per field as arrays
+ - `meta.valid` may be false during initial render before validation runs
+ - Nested fields use dot notation: `defineField('user.profile.name')`
+ - Array field errors use bracket notation: `errors['items[0].name']`
+ - `resetForm({ values: data })` not `resetForm(data)` - wrong structure silently fails
+ - `keepValuesOnUnmount: false` (default) drops values of unmounted fields - set `true` for multi-step forms
+ - Mixing `<Form>` component with `useForm()` creates conflicting contexts - pick one approach
+ - Zod `refine/superRefine` do NOT execute when object keys are missing - always initialize all fields
- </integration>
+ </red_flags>
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md**
**(You MUST use `toTypedSchema()` wrapper when using schema libraries in v4 - raw schemas won't work)**
**(You MUST use `field.key` as iteration key in useFieldArray - NEVER use array index)**
**(You MUST use function form `() => props.name` or `toRef()` in useField for prop reactivity)**
**(You MUST initialize field array values in `initialValues` - undefined arrays cause errors)**
**Failure to follow these rules will break form validation, cause reactivity issues, and corrupt form state.**
</critical_reminders>