Troubleshooting
Common issues and their solutions when working with Vue Hook Form.
Common Mistakes
Field Arrays
Most field array issues stem from violating critical rules. See Field Arrays - Critical Rules for:
- Using dot notation for paths (not bracket notation)
- Using
field.keyfor v-for keys (not index) - Initializing arrays in
defaultValues - Calling
fields()in setup (not in template)
Forgetting .value on Refs
Form state and watched values are Vue refs - access them with .value:
<!-- CORRECT -->
<span v-if="formState.value.errors.email">
{{ formState.value.errors.email }}
</span>
<!-- WRONG - this won't work -->
<span v-if="formState.errors.email">
{{ formState.errors.email }}
</span>Mixing v-model with Uncontrolled Register
Don't use v-model with the default uncontrolled mode:
<!-- CORRECT - uncontrolled mode -->
<input v-bind="register('email')" />
<!-- CORRECT - controlled mode with v-model -->
<script setup>
const { value: email, ...bindings } = register('email', { controlled: true })
</script>
<input v-model="email" v-bind="bindings" />
<!-- WRONG - v-model with uncontrolled register -->
<input v-model="email" v-bind="register('email')" />Frequently Asked Questions
Why isn't my field updating?
Possible causes:
Using uncontrolled mode with reactive data: Uncontrolled inputs read from/write to the DOM directly. If you need reactive updates, use controlled mode:
typescriptconst { value, ...bindings } = register('field', { controlled: true })Not calling getValues() before accessing values: For uncontrolled inputs, use
getValues()to sync DOM state:typescriptconst currentValues = getValues() // Syncs from DOMMissing ref on custom component: Ensure your component forwards the ref:
vue<!-- CustomInput.vue --> <template> <input ref="inputRef" v-bind="$attrs" /> </template> <script setup> import { ref } from 'vue' const inputRef = ref() defineExpose({ focus: () => inputRef.value?.focus() }) </script>
Why do I get validation errors immediately?
Check your validation mode:
const { register } = useForm({
schema,
mode: 'onSubmit', // Only validate on submit (default)
// mode: 'onChange', // Validates on every change - may feel aggressive
// mode: 'onBlur', // Validates when field loses focus
// mode: 'onTouched', // Validates after first touch, then on change
})Using onChange? Consider using delayError to prevent error flash during typing:
const { register } = useForm({
schema,
mode: 'onChange',
delayError: 500, // Wait 500ms before showing errors
})Why is my form slow?
Common performance issues:
Watching all fields unnecessarily:
typescript// SLOW - re-renders on every field change const allValues = watch() // FAST - only watch what you need const email = watch('email')Using controlled mode everywhere: Controlled mode uses Vue reactivity which has overhead. Use uncontrolled (default) for simple inputs.
Large field arrays without virtualization: For 100+ items, consider virtual scrolling:
vue<VirtualList :items="items.value" :item-height="50"> <template #default="{ item }"> <input v-bind="register(`items.${item.index}.name`)" /> </template> </VirtualList>Complex schemas validated on every change: Use
mode: 'onBlur'ormode: 'onSubmit'for complex forms.
How do I handle server errors?
Option 1: Using setError after submission
async function onSubmit(data) {
try {
await api.save(data)
} catch (error) {
if (error.field) {
setError(error.field, { message: error.message })
} else {
setError('root', { message: 'Submission failed' })
}
}
}Option 2: Using the errors option
const serverErrors = ref({})
const { register } = useForm({
schema,
errors: serverErrors, // Merged with validation errors
})
// After API call fails:
serverErrors.value = { email: 'Email already exists' }How do I reset to different values?
// Reset to original defaults
reset()
// Reset to new values
reset({
name: 'New Name',
email: 'new@email.com',
})
// Reset but keep some state
reset(undefined, {
keepErrors: true,
keepDirty: true,
})How do I validate a single field manually?
// Validate single field
const isValid = await trigger('email')
// Validate multiple fields
const areValid = await trigger(['email', 'password'])
// Validate entire form
const formValid = await trigger()How do I access the current value of a field?
// Option 1: getValues (syncs from DOM for uncontrolled)
const email = getValues('email')
// Option 2: watch (reactive, for use in template/computed)
const email = watch('email')
console.log(email.value)
// Option 3: Controlled mode (reactive binding)
const { value: email } = register('email', { controlled: true })Can I use uncontrolled mode with PrimeVue/Vuetify?
Yes, for many components! Vue Hook Form automatically detects input elements inside Vue components.
Works automatically:
- Components where
$elis the input (PrimeVueInputText,InputNumber) - Components where
$elis a wrapper containing an<input>,<select>, or<textarea>
<!-- This works with uncontrolled mode -->
<InputText v-bind="register('username')" />Use controlled mode instead for:
- Components with complex structures (multiple inputs, custom rendering)
- Components that don't use standard
inputevents (some date pickers, autocompletes) - When you need reactive value access during typing
To check if a component is compatible:
// In browser DevTools console, find your component instance
const vm = document.querySelector('.p-inputtext').__vueParentComponent?.proxy
console.log(vm?.$el) // Should be an <input> or contain oneSee Vue Component Library Support for more details.
Why aren't my array operations working?
Array methods return false if the operation was rejected:
const items = fields('items', {
rules: { maxLength: { value: 5, message: 'Max 5 items' } },
})
const success = items.append({ name: '' })
if (!success) {
console.log('Could not add item - max length reached')
}Check return values and ensure you're within min/max constraints.
Debugging Tips
Enable Vue DevTools
Vue DevTools shows reactive state. Look for:
formStateref valueswatch()computed values- Component re-renders
Log Form State
import { watchEffect } from 'vue'
// Log all state changes
watchEffect(() => {
console.log('Form state:', {
errors: formState.value.errors,
isDirty: formState.value.isDirty,
isValid: formState.value.isValid,
})
})Check Field Registration
// After registering, verify the field is tracked
const bindings = register('email')
console.log('Registered field:', bindings)Inspect Zod Schema
// Validate data manually to see all errors
const result = schema.safeParse(formData)
if (!result.success) {
console.log('Validation errors:', result.error.format())
}TypeScript Issues
Path Autocomplete Not Working
Ensure your schema type is inferred correctly:
// CORRECT - type is inferred
const schema = z.object({
email: z.string().email(),
})
const { register } = useForm({ schema })
register('email') // Autocomplete works
// ISSUE - schema as const may lose type info
const schema = { ... } as const // May not work as expectedType Error on Dynamic Paths
For dynamic paths (like array indices), you may need type assertions:
// If TypeScript complains about dynamic paths:
register(`items.${index}.name` as `items.${number}.name`)
// Or use a typed helper
function itemPath(index: number, field: string) {
return `items.${index}.${field}` as const
}
register(itemPath(0, 'name'))Generic Component Types
For reusable components, use generics:
import type { ZodType } from 'zod'
import type { Control, FormPath } from '@vuehookform/core'
const props = defineProps<{
control: Control<ZodType>
name: FormPath<typeof props.control>
}>()Getting Help
If you're still stuck:
- Check the API Reference for detailed method signatures
- Look at Examples for working code
- Search or open an issue on GitHub
Next Steps
- Review Best Practices for optimization tips
- Explore Patterns for common form architectures
